Add copy button to sso callback urls, e2e unit testing for OIDC, stub out other auth tests

This commit is contained in:
Rory Powell 2022-11-16 11:34:16 +00:00
parent e57c115f86
commit 9c169087e6
28 changed files with 578 additions and 333 deletions

View File

@ -16,8 +16,7 @@
"dist",
"public",
"*.spec.js",
"bundle.js",
"coverage"
"bundle.js"
],
"plugins": ["svelte3"],
"extends": ["eslint:recommended"],

View File

@ -1 +0,0 @@
jest.mock("node-fetch", () => jest.fn())

View File

@ -56,7 +56,7 @@
"@types/chance": "1.1.3",
"@types/ioredis": "4.28.0",
"@types/jest": "27.5.1",
"@types/koa": "2.0.52",
"@types/koa": "2.13.4",
"@types/lodash": "4.14.180",
"@types/node": "14.18.20",
"@types/node-fetch": "2.6.1",
@ -68,7 +68,7 @@
"chance": "1.1.3",
"ioredis-mock": "5.8.0",
"jest": "28.1.1",
"koa": "2.7.0",
"koa": "2.13.4",
"nodemon": "2.0.16",
"pouchdb-adapter-memory": "7.2.2",
"timekeeper": "2.2.0",

View File

@ -0,0 +1,134 @@
import * as matchers from "../matchers"
import { structures } from "../../../tests"
describe("matchers", () => {
it("matches by path and method", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(true)
})
it("wildcards path", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests/id/something/else"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(true)
})
it("doesn't wildcard path with strict", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
strict: true,
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests/id/something/else"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(false)
})
it("matches with param", () => {
const pattern = [
{
route: "/api/tests/:testId",
method: "GET",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests/id"
ctx.request.method = "GET"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(true)
})
// TODO: Support the below behaviour
// Strict does not work when a param is present
// it("matches with param with strict", () => {
// const pattern = [{
// route: "/api/tests/:testId",
// method: "GET",
// strict: true
// }]
// const ctx = structures.koa.newContext()
// ctx.request.url = "/api/tests/id"
// ctx.request.method = "GET"
//
// const built = matchers.buildMatcherRegex(pattern)
//
// expect(!!matchers.matches(ctx, built)).toBe(true)
// })
it("doesn't match by path", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/unknown"
ctx.request.method = "POST"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(false)
})
it("doesn't match by method", () => {
const pattern = [
{
route: "/api/tests",
method: "POST",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests"
ctx.request.method = "GET"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(false)
})
it("matches by path and wildcard method", () => {
const pattern = [
{
route: "/api/tests",
method: "ALL",
},
]
const ctx = structures.koa.newContext()
ctx.request.url = "/api/tests"
ctx.request.method = "GET"
const built = matchers.buildMatcherRegex(pattern)
expect(!!matchers.matches(ctx, built)).toBe(true)
})
})

View File

@ -231,12 +231,18 @@ export const getTenantIDFromCtx = (
const match = ctx.matched.find(
(m: any) => !!m.paramNames.find((p: any) => p.name === "tenantId")
)
// get the raw path url - without any query params
const ctxUrl = ctx.originalUrl
let url
if (ctxUrl.includes("?")) {
url = ctxUrl.split("?")[0]
} else {
url = ctxUrl
}
if (match) {
const params = match.params(
ctx.originalUrl,
match.captures(ctx.originalUrl),
{}
)
const params = match.params(url, match.captures(url), {})
if (params.tenantId) {
return params.tenantId
}

View File

@ -30,8 +30,8 @@ async function resolveAppUrl(ctx: BBContext) {
let possibleAppUrl = `/${appUrl.toLowerCase()}`
let tenantId = tenancy.getTenantId()
if (!env.SELF_HOSTED) {
// always use the tenant id from the subdomain in cloud
if (env.MULTI_TENANCY) {
// always use the tenant id from the subdomain in multi tenancy
// this ensures the logged-in user tenant id doesn't overwrite
// e.g. in the case of viewing a public app while already logged-in to another tenant
tenantId = tenancy.getTenantIDFromCtx(ctx, {

View File

@ -0,0 +1,4 @@
const mockFetch = jest.fn()
jest.mock("node-fetch", () => mockFetch)
export default mockFetch

View File

@ -2,3 +2,4 @@ import "./posthog"
import "./events"
export * as accounts from "./accounts"
export * as date from "./date"
export { default as fetch } from "./fetch"

View File

@ -1,5 +1,13 @@
import { createMockContext } from "@shopify/jest-koa-mocks"
import { BBContext } from "@budibase/types"
export const newContext = () => {
return createMockContext()
export const newContext = (): BBContext => {
const ctx = createMockContext()
return {
...ctx,
request: {
...ctx.request,
body: {},
},
}
}

View File

@ -1079,7 +1079,7 @@
dependencies:
"@types/koa" "*"
"@types/koa@*":
"@types/koa@*", "@types/koa@2.13.4":
version "2.13.4"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b"
integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==
@ -1093,18 +1093,6 @@
"@types/koa-compose" "*"
"@types/node" "*"
"@types/koa@2.0.52":
version "2.0.52"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.0.52.tgz#7dd11de4189ab339ad66c4ccad153716b14e525f"
integrity sha512-cp/GTOhOYwomlSKqEoG0kaVEVJEzP4ojYmfa7EKaGkmkkRwJ4B/1VBLbQZ49Z+WJNvzXejQB/9GIKqMo9XLgFQ==
dependencies:
"@types/accepts" "*"
"@types/cookies" "*"
"@types/http-assert" "*"
"@types/keygrip" "*"
"@types/koa-compose" "*"
"@types/node" "*"
"@types/lodash@4.14.180":
version "4.14.180"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
@ -1478,11 +1466,6 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
any-promise@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
anymatch@^3.0.3, anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
@ -2056,14 +2039,6 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
dependencies:
safe-buffer "~5.1.1"
cookies@~0.7.1:
version "0.7.3"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.3.tgz#7912ce21fbf2e8c2da70cf1c3f351aecf59dadfa"
integrity sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==
dependencies:
depd "~1.1.2"
keygrip "~1.0.3"
cookies@~0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
@ -2134,13 +2109,6 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@~3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debuglog@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
@ -2201,7 +2169,7 @@ denque@^1.1.0:
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf"
integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==
depd@^1.1.0, depd@^1.1.2, depd@~1.1.2:
depd@^1.1.0, depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
@ -2353,11 +2321,6 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-inject@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37"
integrity sha512-JM8N6PytDbmIYm1IhPWlo8vr3NtfjhDY/1MhD/a5b/aad/USE8a0+NsqE9d5n+GVGmuNkPQWm4bFQWv18d8tMg==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@ -3602,11 +3565,6 @@ jws@^3.0.0, jws@^3.1.4, jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
keygrip@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc"
integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==
keygrip@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
@ -3626,26 +3584,11 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
koa-compose@^3.0.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
integrity sha512-8gen2cvKHIZ35eDEik5WOo8zbVp9t4cP8p4hW4uE55waxolLRexKKrqfCpwhGVppnB40jWeF8bZeTVg99eZgPw==
dependencies:
any-promise "^1.1.0"
koa-compose@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
koa-convert@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0"
integrity sha512-K9XqjmEDStGX09v3oxR7t5uPRy0jqJdvodHa6wxWTHrTfDq0WUNnYTOOUZN6g8OM8oZQXprQASbiIXG2Ez8ehA==
dependencies:
co "^4.6.0"
koa-compose "^3.0.0"
koa-convert@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5"
@ -3654,11 +3597,6 @@ koa-convert@^2.0.0:
co "^4.6.0"
koa-compose "^4.1.0"
koa-is-json@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
integrity sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw==
koa-passport@4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/koa-passport/-/koa-passport-4.1.4.tgz#5f1665c1c2a37ace79af9f970b770885ca30ccfa"
@ -3666,37 +3604,7 @@ koa-passport@4.1.4:
dependencies:
passport "^0.4.0"
koa@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.7.0.tgz#7e00843506942b9d82c6cc33749f657c6e5e7adf"
integrity sha512-7ojD05s2Q+hFudF8tDLZ1CpCdVZw8JQELWSkcfG9bdtoTDzMmkRF6BQBU7JzIzCCOY3xd3tftiy/loHBUYaY2Q==
dependencies:
accepts "^1.3.5"
cache-content-type "^1.0.0"
content-disposition "~0.5.2"
content-type "^1.0.4"
cookies "~0.7.1"
debug "~3.1.0"
delegates "^1.0.0"
depd "^1.1.2"
destroy "^1.0.4"
error-inject "^1.0.0"
escape-html "^1.0.3"
fresh "~0.5.2"
http-assert "^1.3.0"
http-errors "^1.6.3"
is-generator-function "^1.0.7"
koa-compose "^4.1.0"
koa-convert "^1.2.0"
koa-is-json "^1.0.0"
on-finished "^2.3.0"
only "~0.0.2"
parseurl "^1.3.2"
statuses "^1.5.0"
type-is "^1.6.16"
vary "^1.1.2"
koa@^2.13.4:
koa@2.13.4, koa@^2.13.4:
version "2.13.4"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e"
integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==
@ -4079,11 +3987,6 @@ mkdirp@^1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"

View File

@ -20,11 +20,12 @@
Toggle,
Tag,
Tags,
Icon,
Helpers,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { API } from "api"
import { organisation, admin } from "stores/portal"
import { Helpers } from "@budibase/bbui"
const ConfigTypes = {
Google: "google",
@ -40,7 +41,9 @@
// Indicate to user that callback is based on platform url
// If there is an existing value, indicate that it may be removed to return to default behaviour
$: googleCallbackTooltip = googleCallbackReadonly
$: googleCallbackTooltip = $admin.cloud
? null
: googleCallbackReadonly
? "Vist the organisation page to update the platform URL"
: "Leave blank to use the default callback URL"
@ -54,6 +57,7 @@
readonly: googleCallbackReadonly,
tooltip: googleCallbackTooltip,
placeholder: $organisation.googleCallbackUrl,
copyButton: true,
},
],
}
@ -66,9 +70,12 @@
{
name: "callbackURL",
readonly: true,
tooltip: "Vist the organisation page to update the platform URL",
tooltip: $admin.cloud
? null
: "Vist the organisation page to update the platform URL",
label: "Callback URL",
placeholder: $organisation.oidcCallbackUrl,
copyButton: true,
},
],
}
@ -231,6 +238,11 @@
},
]
const copyToClipboard = async value => {
await Helpers.copyToClipboard(value)
notifications.success("Copied")
}
onMount(async () => {
try {
await organisation.init()
@ -336,11 +348,23 @@
{#each GoogleConfigFields.Google as field}
<div class="form-row">
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input
bind:value={providers.google.config[field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
<div class="inputContainer">
<div class="input">
<Input
bind:value={providers.google.config[field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
</div>
{#if field.copyButton}
<div
class="copy"
on:click={() => copyToClipboard(field.placeholder)}
>
<Icon size="S" name="Copy" />
</div>
{/if}
</div>
</div>
{/each}
<div class="form-row">
@ -375,12 +399,23 @@
{#each OIDCConfigFields.Oidc as field}
<div class="form-row">
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<Input
bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
dataCy={field.name}
/>
<div class="inputContainer">
<div class="input">
<Input
bind:value={providers.oidc.config.configs[0][field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
</div>
{#if field.copyButton}
<div
class="copy"
on:click={() => copyToClipboard(field.placeholder)}
>
<Icon size="S" name="Copy" />
</div>
{/if}
</div>
</div>
{/each}
</Layout>
@ -557,4 +592,16 @@
.provider-title span {
flex: 1 1 auto;
}
.inputContainer {
display: flex;
flex-direction: row;
}
.input {
flex: 1;
}
.copy {
display: flex;
align-items: center;
margin-left: 10px;
}
</style>

View File

@ -31,5 +31,5 @@ export interface ScannedSession {
export interface PlatformLogoutOpts {
ctx: BBContext
userId: string
keepActiveSession: boolean
keepActiveSession?: boolean
}

View File

@ -1 +0,0 @@
jest.mock("node-fetch", () => jest.fn())

View File

@ -0,0 +1,57 @@
import * as jwt from "jsonwebtoken"
const mockOAuth2 = {
getOAuthAccessToken: (code: string, p: any, cb: any) => {
const err = null
const accessToken = "access_token"
const refreshToken = "refresh_token"
const exp = new Date()
exp.setDate(exp.getDate() + 1)
const iat = new Date()
iat.setDate(iat.getDate() - 1)
const claims = {
iss: "test",
sub: "sub",
aud: "clientId",
exp: exp.getTime() / 1000,
iat: iat.getTime() / 1000,
email: "oauth@example.com",
}
const idToken = jwt.sign(claims, "secret")
const params = {
id_token: idToken,
}
return cb(err, accessToken, refreshToken, params)
},
_request: (
method: string,
url: string,
headers: any,
postBody: any,
accessToken: string,
cb: any
) => {
const err = null
const body = {
sub: "sub",
user_id: "userId",
name: "OAuth",
family_name: "2",
given_name: "OAuth",
middle_name: "",
}
const res = {}
return cb(err, JSON.stringify(body), res)
},
}
const oauth = {
OAuth2: jest.fn(() => mockOAuth2),
}
export = oauth

View File

@ -71,9 +71,11 @@
},
"devDependencies": {
"@types/jest": "26.0.23",
"@types/jsonwebtoken": "8.5.1",
"@types/koa": "2.13.4",
"@types/koa__router": "8.0.11",
"@types/node": "14.18.20",
"@types/node-fetch": "2.6.1",
"@types/pouchdb": "6.4.0",
"@types/uuid": "8.3.4",
"@typescript-eslint/parser": "5.12.0",

View File

@ -1,4 +1,4 @@
const core = require("@budibase/backend-core")
import core from "@budibase/backend-core"
const { Configs, EmailTemplatePurpose } = require("../../../constants")
const { sendEmail, isEmailConfigured } = require("../../../utilities/email")
const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils

View File

@ -29,11 +29,13 @@ function buildResetUpdateValidation() {
}
router
// PASSWORD
.post(
"/api/global/auth/:tenantId/login",
buildAuthValidation(),
authController.authenticate
)
.post("/api/global/auth/logout", authController.logout)
.post(
"/api/global/auth/:tenantId/reset",
buildResetValidation(),
@ -44,36 +46,43 @@ router
buildResetUpdateValidation(),
authController.resetUpdate
)
.post("/api/global/auth/logout", authController.logout)
// INIT
.post("/api/global/auth/init", authController.setInitInfo)
.get("/api/global/auth/init", authController.getInitInfo)
.get("/api/global/auth/:tenantId/google", authController.googlePreAuth)
// DATASOURCE - MULTI TENANT
.get(
"/api/global/auth/:tenantId/datasource/:provider",
authController.datasourcePreAuth
)
// single tenancy endpoint
.get("/api/global/auth/google/callback", authController.googleAuth)
.get(
"/api/global/auth/datasource/:provider/callback",
authController.datasourceAuth
)
// multi-tenancy endpoint
.get("/api/global/auth/:tenantId/google/callback", authController.googleAuth)
.get(
"/api/global/auth/:tenantId/datasource/:provider/callback",
authController.datasourceAuth
)
// DATASOURCE - SINGLE TENANT - DEPRECATED
.get(
"/api/global/auth/datasource/:provider/callback",
authController.datasourceAuth
)
// GOOGLE - MULTI TENANT
.get("/api/global/auth/:tenantId/google", authController.googlePreAuth)
.get("/api/global/auth/:tenantId/google/callback", authController.googleAuth)
// GOOGLE - SINGLE TENANT - DEPRECATED
.get("/api/global/auth/google/callback", authController.googleAuth)
.get("/api/admin/auth/google/callback", authController.googleAuth)
// OIDC - MULTI TENANT
.get(
"/api/global/auth/:tenantId/oidc/configs/:configId",
authController.oidcPreAuth
)
// single tenancy endpoint
.get("/api/global/auth/oidc/callback", authController.oidcAuth)
// multi-tenancy endpoint
.get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth)
// deprecated - used by the default system before tenancy
.get("/api/admin/auth/google/callback", authController.googleAuth)
// OIDC - SINGLE TENANT - DEPRECATED
.get("/api/global/auth/oidc/callback", authController.oidcAuth)
.get("/api/admin/auth/oidc/callback", authController.oidcAuth)
module.exports = router

View File

@ -3,6 +3,12 @@ import { TestConfiguration, mocks } from "../../../../tests"
const sendMailMock = mocks.email.mock()
import { events } from "@budibase/backend-core"
const expectSetAuthCookie = (res: any) => {
expect(
res.get("Set-Cookie").find((c: string) => c.startsWith("budibase:auth"))
).toBeDefined()
}
describe("/api/global/auth", () => {
const config = new TestConfiguration()
@ -18,92 +24,155 @@ describe("/api/global/auth", () => {
jest.clearAllMocks()
})
it("should logout", async () => {
await config.api.auth.logout()
expect(events.auth.logout).toBeCalledTimes(1)
})
it("should be able to generate password reset email", async () => {
const { res, code } = await config.api.auth.requestPasswordReset(
sendMailMock
)
const user = await config.getUser("test@test.com")
expect(res.body).toEqual({
message: "Please check your email for a reset link.",
describe("password", () => {
describe("POST /api/global/auth/:tenantId/login", () => {
it("should login", () => {})
})
expect(sendMailMock).toHaveBeenCalled()
expect(code).toBeDefined()
expect(events.user.passwordResetRequested).toBeCalledTimes(1)
expect(events.user.passwordResetRequested).toBeCalledWith(user)
describe("POST /api/global/auth/logout", () => {
it("should logout", async () => {
await config.api.auth.logout()
expect(events.auth.logout).toBeCalledTimes(1)
// TODO: Verify sessions deleted
})
})
describe("POST /api/global/auth/:tenantId/reset", () => {
it("should generate password reset email", async () => {
const { res, code } = await config.api.auth.requestPasswordReset(
sendMailMock
)
const user = await config.getUser("test@test.com")
expect(res.body).toEqual({
message: "Please check your email for a reset link.",
})
expect(sendMailMock).toHaveBeenCalled()
expect(code).toBeDefined()
expect(events.user.passwordResetRequested).toBeCalledTimes(1)
expect(events.user.passwordResetRequested).toBeCalledWith(user)
})
})
describe("POST /api/global/auth/:tenantId/reset/update", () => {
it("should reset password", async () => {
const { code } = await config.api.auth.requestPasswordReset(
sendMailMock
)
const user = await config.getUser("test@test.com")
delete user.password
const res = await config.api.auth.updatePassword(code)
expect(res.body).toEqual({ message: "password reset successfully." })
expect(events.user.passwordReset).toBeCalledTimes(1)
expect(events.user.passwordReset).toBeCalledWith(user)
// TODO: Login using new password
})
})
})
it("should allow resetting user password with code", async () => {
const { code } = await config.api.auth.requestPasswordReset(sendMailMock)
const user = await config.getUser("test@test.com")
delete user.password
describe("init", () => {
describe("POST /api/global/auth/init", () => {})
const res = await config.api.auth.updatePassword(code)
describe("GET /api/global/auth/init", () => {})
})
expect(res.body).toEqual({ message: "password reset successfully." })
expect(events.user.passwordReset).toBeCalledTimes(1)
expect(events.user.passwordReset).toBeCalledWith(user)
describe("datasource", () => {
// MULTI TENANT
describe("GET /api/global/auth/:tenantId/datasource/:provider", () => {})
describe("GET /api/global/auth/:tenantId/datasource/:provider/callback", () => {})
// SINGLE TENANT
describe("GET /api/global/auth/datasource/:provider/callback", () => {})
})
describe("google", () => {
// MULTI TENANT
describe("GET /api/global/auth/:tenantId/google", () => {})
describe("GET /api/global/auth/:tenantId/google/callback", () => {})
// SINGLE TENANT
describe("GET /api/global/auth/google/callback", () => {})
describe("GET /api/admin/auth/google/callback", () => {})
})
describe("oidc", () => {
const auth = require("@budibase/backend-core/auth")
const passportSpy = jest.spyOn(auth.passport, "authenticate")
let oidcConf
let chosenConfig: any
let configId: string
// mock the oidc strategy implementation and return value
let strategyFactory = jest.fn()
let mockStrategyReturn = jest.fn()
let mockStrategyConfig = jest.fn()
auth.oidc.fetchStrategyConfig = mockStrategyConfig
strategyFactory.mockReturnValue(mockStrategyReturn)
auth.oidc.strategyFactory = strategyFactory
beforeEach(async () => {
oidcConf = await config.saveOIDCConfig()
chosenConfig = oidcConf.config.configs[0]
configId = chosenConfig.uuid
mockStrategyConfig.mockReturnValue(chosenConfig)
jest.clearAllMocks()
mockGetWellKnownConfig()
// see: __mocks__/oauth
// for associated mocking inside passport
})
afterEach(() => {
expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function))
})
const generateOidcConfig = async () => {
const oidcConf = await config.saveOIDCConfig()
const chosenConfig = oidcConf.config.configs[0]
return chosenConfig.uuid
}
describe("oidc configs", () => {
it("should load strategy and delegate to passport", async () => {
await config.api.configs.getOIDCConfig(configId)
const mockGetWellKnownConfig = () => {
mocks.fetch.mockReturnValue({
ok: true,
json: () => ({
issuer: "test",
authorization_endpoint: "http://localhost/auth",
token_endpoint: "http://localhost/token",
userinfo_endpoint: "http://localhost/userinfo",
}),
})
}
expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
scope: ["profile", "email", "offline_access"],
})
expect(passportSpy.mock.calls.length).toBe(1)
// MULTI TENANT
describe("GET /api/global/auth/:tenantId/oidc/configs/:configId", () => {
it("redirects to auth provider", async () => {
const configId = await generateOidcConfig()
const res = await config.api.configs.getOIDCConfig(configId)
expect(res.status).toBe(302)
const location: string = res.get("location")
expect(
location.startsWith(
"http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2Fdefault%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access"
)
).toBe(true)
})
})
describe("oidc callback", () => {
it("should load strategy and delegate to passport", async () => {
await config.api.configs.OIDCCallback(configId)
describe("GET /api/global/auth/:tenantId/oidc/callback", () => {
it("logs in", async () => {
const configId = await generateOidcConfig()
const preAuthRes = await config.api.configs.getOIDCConfig(configId)
expect(passportSpy).toBeCalledWith(
mockStrategyReturn,
{
successRedirect: "/",
failureRedirect: "/error",
},
expect.anything()
)
expect(passportSpy.mock.calls.length).toBe(1)
const res = await config.api.configs.OIDCCallback(configId, preAuthRes)
expect(events.auth.login).toBeCalledWith("oidc")
expect(events.auth.login).toBeCalledTimes(1)
expect(res.status).toBe(302)
const location: string = res.get("location")
expect(location).toBe("/")
expectSetAuthCookie(res)
})
})
// SINGLE TENANT
describe("GET /api/global/auth/oidc/callback", () => {})
describe("GET /api/global/auth/oidc/callback", () => {})
describe("GET /api/admin/auth/oidc/callback", () => {})
})
})

View File

@ -1,7 +1,7 @@
import { TestConfiguration } from "../../../../tests"
import { tenancy } from "@budibase/backend-core"
describe("/api/global/workspaces", () => {
describe("/api/global/tenants", () => {
const config = new TestConfiguration()
beforeAll(async () => {

View File

@ -171,8 +171,11 @@ class TestConfiguration {
}
cookieHeader(cookies: any) {
if (!Array.isArray(cookies)) {
cookies = [cookies]
}
return {
Cookie: [cookies],
Cookie: cookies,
}
}
@ -288,11 +291,6 @@ class TestConfiguration {
// CONFIGS - OIDC
getOIDConfigCookie(configId: string) {
const token = auth.jwt.sign(configId, env.JWT_SECRET)
return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]])
}
async saveOIDCConfig() {
await this.deleteConfig(Configs.OIDC)
const config = structures.configs.oidc()

View File

@ -23,10 +23,20 @@ export class ConfigAPI extends TestAPI {
.expect(200)
}
OIDCCallback = (configId: string) => {
OIDCCallback = (configId: string, preAuthRes: any) => {
const cookie = this.config.cookieHeader(preAuthRes.get("set-cookie"))
const setKoaSession = cookie.Cookie.find((c: string) =>
c.includes("koa:sess")
)
const koaSession = setKoaSession.split("=")[1] + "=="
const sessionContent = JSON.parse(
Buffer.from(koaSession, "base64").toString("utf-8")
)
const handle = sessionContent["openidconnect:localhost"].state.handle
return this.request
.get(`/api/global/auth/${this.config.getTenantId()}/oidc/callback`)
.set(this.config.getOIDConfigCookie(configId))
.query({ code: "test", state: handle })
.set(cookie)
}
getOIDCConfig = (configId: string) => {

View File

@ -1284,6 +1284,13 @@
resolved "https://registry.yarnpkg.com/@types/json-buffer/-/json-buffer-3.0.0.tgz#85c1ff0f0948fc159810d4b5be35bf8c20875f64"
integrity sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==
"@types/jsonwebtoken@8.5.1":
version "8.5.1"
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#56958cb2d80f6d74352bd2e501a018e2506a8a84"
integrity sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw==
dependencies:
"@types/node" "*"
"@types/keygrip@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
@ -1334,6 +1341,14 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node-fetch@2.6.1":
version "2.6.1"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*":
version "17.0.41"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.41.tgz#1607b2fd3da014ae5d4d1b31bc792a39348dfb9b"
@ -3412,6 +3427,15 @@ forever-agent@~0.6.1:
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"

View File

@ -114,8 +114,6 @@ export default class AppApi {
return [response, json]
}
async delete(appId: string): Promise<[Response, any]> {
const response = await this.api.del(`/applications/${appId}`)
const json = await response.json()
@ -123,7 +121,11 @@ export default class AppApi {
return [response, json]
}
async update(appId: string, oldName: string, body: any): Promise<[Response, Application]> {
async update(
appId: string,
oldName: string,
body: any
): Promise<[Response, Application]> {
const response = await this.api.put(`/applications/${appId}`, { body })
const json = await response.json()
expect(response).toHaveStatusCode(200)
@ -142,7 +144,6 @@ export default class AppApi {
const json = await response.json()
expect(response).toHaveStatusCode(200)
if (screenExists) {
expect(json.routes["/test"]).toBeTruthy()
} else {
expect(json.routes["/test"]).toBeUndefined()

View File

@ -46,9 +46,7 @@ export default class TablesApi {
const response = await this.api.del(`/tables/${id}/${revId}`)
const json = await response.json()
expect(response).toHaveStatusCode(200)
expect(json.message).toEqual(
`Table ${id} deleted.`
)
expect(json.message).toEqual(`Table ${id} deleted.`)
return [response, json]
}
}

View File

@ -1,7 +1,6 @@
import generator from "../../generator"
import { Application } from "@budibase/server/api/controllers/public/mapping/types"
const generate = (
overrides: Partial<Application> = {}
): Partial<Application> => ({

View File

@ -48,7 +48,8 @@ describe("Internal API - Application creation, update, publish and delete", () =
})
config.applications.api.appId = app.appId
const [appPackageResponse, appPackageJson] = await config.applications.getAppPackage(<string>app.appId)
const [appPackageResponse, appPackageJson] =
await config.applications.getAppPackage(<string>app.appId)
expect(appPackageJson.application.name).toEqual(app.name)
expect(appPackageJson.application.version).toEqual(app.version)
expect(appPackageJson.application.url).toEqual(app.url)
@ -72,7 +73,6 @@ describe("Internal API - Application creation, update, publish and delete", () =
config.applications.api.appId = db.getProdAppID(app.appId)
await config.applications.canRender()
// unpublish app
await config.applications.unpublish(<string>app.appId)
})
@ -109,22 +109,16 @@ describe("Internal API - Application creation, update, publish and delete", () =
config.applications.api.appId = app.appId
await config.applications.update(
<string>app.appId,
<string>app.name,
{
name: generator.word(),
}
)
await config.applications.update(<string>app.appId, <string>app.name, {
name: generator.word(),
})
})
it("POST - Revert Changes without changes", async () => {
const app = await config.applications.create(generateApp())
config.applications.api.appId = app.appId
await config.applications.revertUnpublished(
<string>app.appId
)
await config.applications.revertUnpublished(<string>app.appId)
})
it("POST - Revert Changes", async () => {
@ -134,20 +128,14 @@ describe("Internal API - Application creation, update, publish and delete", () =
// publish app
await config.applications.publish(<string>app.url)
// Change/add component to the app
await config.screen.create(
generateScreen("BASIC")
)
await config.screen.create(generateScreen("BASIC"))
// // Revert the app to published state
await config.applications.revertPublished(
<string>app.appId
)
await config.applications.revertPublished(<string>app.appId)
// Check screen is removed
await config.applications.getRoutes()
})
it("DELETE - Delete an application", async () => {
@ -155,5 +143,4 @@ describe("Internal API - Application creation, update, publish and delete", () =
await config.applications.delete(<string>app.appId)
})
})

View File

@ -38,9 +38,7 @@ describe("Internal API - /screens endpoints", () => {
// Create Screen
appConfig.applications.api.appId = app.appId
await config.screen.create(
generateScreen("BASIC")
)
await config.screen.create(generateScreen("BASIC"))
// Check screen exists
await appConfig.applications.getRoutes(true)
@ -58,6 +56,5 @@ describe("Internal API - /screens endpoints", () => {
// Delete Screen
await config.screen.delete(screen._id!, screen._rev!)
})
})

View File

@ -3,93 +3,87 @@ import { Application } from "@budibase/server/api/controllers/public/mapping/typ
import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient"
import generator from "../../../config/generator"
import {
generateTable,
generateNewColumnForTable,
generateTable,
generateNewColumnForTable,
} from "../../../config/internal-api/fixtures/table"
import { generateNewRowForTable } from "../../../config/internal-api/fixtures/rows"
describe("Internal API - Application creation, update, publish and delete", () => {
const api = new InternalAPIClient()
const config = new TestConfiguration<Application>(api)
const api = new InternalAPIClient()
const config = new TestConfiguration<Application>(api)
beforeAll(async () => {
await config.beforeAll()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
async function createAppFromTemplate() {
return config.applications.create({
name: generator.word(),
url: `/${generator.word()}`,
useTemplate: "true",
templateName: "Near Miss Register",
templateKey: "app/near-miss-register",
templateFile: undefined,
})
}
afterAll(async () => {
await config.afterAll()
})
it("Operations on Tables", async () => {
// create the app
const appName = generator.word()
const app = await createAppFromTemplate()
config.applications.api.appId = app.appId
async function createAppFromTemplate() {
return config.applications.create({
name: generator.word(),
url: `/${generator.word()}`,
useTemplate: "true",
templateName: "Near Miss Register",
templateKey: "app/near-miss-register",
templateFile: undefined,
})
// Get current tables: expect 2 in this template
await config.tables.getAll(2)
// Add new table
const [createdTableResponse, createdTableData] = await config.tables.save(
generateTable()
)
//Table was added
await config.tables.getAll(3)
//Get information about the table
await config.tables.getTableById(<string>createdTableData._id)
//Add Column to table
const newColumn = generateNewColumnForTable(createdTableData)
const [addColumnResponse, addColumnData] = await config.tables.save(
newColumn,
true
)
//Add Row to table
const newRow = generateNewRowForTable(<string>addColumnData._id)
await config.rows.add(<string>addColumnData._id, newRow)
//Get Row from table
const [getRowResponse, getRowData] = await config.rows.getAll(
<string>addColumnData._id
)
//Delete Row from table
const rowToDelete = {
rows: [getRowData[0]],
}
const [deleteRowResponse, deleteRowData] = await config.rows.delete(
<string>addColumnData._id,
rowToDelete
)
expect(deleteRowData[0]._id).toEqual(getRowData[0]._id)
it("Operations on Tables", async () => {
// create the app
const appName = generator.word()
const app = await createAppFromTemplate()
config.applications.api.appId = app.appId
//Delete the table
const [deleteTableResponse, deleteTable] = await config.tables.delete(
<string>addColumnData._id,
<string>addColumnData._rev
)
// Get current tables: expect 2 in this template
await config.tables.getAll(2)
// Add new table
const [createdTableResponse, createdTableData] = await config.tables.save(
generateTable()
)
//Table was added
await config.tables.getAll(3)
//Get information about the table
await config.tables.getTableById(
<string>createdTableData._id
)
//Add Column to table
const newColumn = generateNewColumnForTable(createdTableData)
const [addColumnResponse, addColumnData] = await config.tables.save(
newColumn,
true
)
//Add Row to table
const newRow = generateNewRowForTable(<string>addColumnData._id)
await config.rows.add(
<string>addColumnData._id,
newRow
)
//Get Row from table
const [getRowResponse, getRowData] = await config.rows.getAll(
<string>addColumnData._id
)
//Delete Row from table
const rowToDelete = {
rows: [getRowData[0]],
}
const [deleteRowResponse, deleteRowData] = await config.rows.delete(
<string>addColumnData._id,
rowToDelete
)
expect(deleteRowData[0]._id).toEqual(getRowData[0]._id)
//Delete the table
const [deleteTableResponse, deleteTable] = await config.tables.delete(
<string>addColumnData._id,
<string>addColumnData._rev
)
//Table was deleted
await config.tables.getAll(2)
})
//Table was deleted
await config.tables.getAll(2)
})
})