Browse Source

Merge pull request #1538 from Budibase/fix/lockdown-admin

Locking down administration endpoints and routes
master
Michael Drury 5 years ago
committed by GitHub
parent
commit
0b728f07fa
  1. 1
      packages/auth/src/middleware/authenticated.js
  2. 46
      packages/builder/src/pages/builder/portal/_layout.svelte
  3. 18
      packages/builder/src/pages/builder/portal/manage/_layout.svelte
  4. 7
      packages/builder/src/pages/builder/portal/manage/auth/_layout.svelte
  5. 1
      packages/builder/src/pages/builder/portal/manage/email/_layout.svelte
  6. 44
      packages/builder/src/pages/builder/portal/manage/users/[userId].svelte
  7. 7
      packages/builder/src/pages/builder/portal/manage/users/_layout.svelte
  8. 4
      packages/builder/src/pages/builder/portal/manage/users/index.svelte
  9. 2
      packages/builder/src/pages/builder/portal/settings/index.svelte
  10. 118
      packages/builder/src/pages/builder/portal/settings/organisation.svelte
  11. 10
      packages/builder/src/stores/portal/auth.js
  12. 2
      packages/worker/src/api/routes/admin/email.js
  13. 20
      packages/worker/src/api/routes/admin/users.js
  14. 5
      packages/worker/src/middleware/adminOnly.js

1
packages/auth/src/middleware/authenticated.js

@ -43,6 +43,7 @@ module.exports = (noAuthPatterns = [], opts) => {
// this is an internal request, no user made it // this is an internal request, no user made it
if (apiKey && apiKey === env.INTERNAL_API_KEY) { if (apiKey && apiKey === env.INTERNAL_API_KEY) {
ctx.isAuthenticated = true ctx.isAuthenticated = true
ctx.internal = true
} else if (authCookie) { } else if (authCookie) {
try { try {
const db = database.getDB(StaticDatabases.GLOBAL.name) const db = database.getDB(StaticDatabases.GLOBAL.name)

46
packages/builder/src/pages/builder/portal/_layout.svelte

@ -20,18 +20,40 @@
let userInfoModal let userInfoModal
let changePasswordModal let changePasswordModal
const menu = [ $: menu = buildMenu($auth.isAdmin)
{ title: "Apps", href: "/builder/portal/apps" },
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" }, const buildMenu = admin => {
{ title: "Auth", href: "/builder/portal/manage/auth" }, let menu = [{ title: "Apps", href: "/builder/portal/apps" }]
{ title: "Email", href: "/builder/portal/manage/email" }, if (admin) {
{ menu = menu.concat([
title: "Organisation", {
href: "/builder/portal/settings/organisation", title: "Users",
heading: "Settings", href: "/builder/portal/manage/users",
}, heading: "Manage",
{ title: "Theming", href: "/builder/portal/settings/theming" }, },
] { title: "Auth", href: "/builder/portal/manage/auth" },
{ title: "Email", href: "/builder/portal/manage/email" },
{
title: "Organisation",
href: "/builder/portal/settings/organisation",
heading: "Settings",
},
{
title: "Theming",
href: "/builder/portal/settings/theming",
},
])
} else {
menu = menu.concat([
{
title: "Theming",
href: "/builder/portal/settings/theming",
heading: "Settings",
},
])
}
return menu
}
onMount(async () => { onMount(async () => {
// Prevent non-builders from accessing the portal // Prevent non-builders from accessing the portal

18
packages/builder/src/pages/builder/portal/manage/_layout.svelte

@ -0,0 +1,18 @@
<script>
import { Page } from "@budibase/bbui"
import { auth } from "stores/portal"
import { redirect } from "@roxi/routify"
// Only admins allowed here
$: {
if (!$auth.isAdmin) {
$redirect("../")
}
}
</script>
{#if $auth.isAdmin}
<Page>
<slot />
</Page>
{/if}

7
packages/builder/src/pages/builder/portal/manage/auth/_layout.svelte

@ -1,7 +0,0 @@
<script>
import { Page } from "@budibase/bbui"
</script>
<Page>
<slot />
</Page>

1
packages/builder/src/pages/builder/portal/manage/email/_layout.svelte

@ -1,5 +1,6 @@
<script> <script>
import { email } from "stores/portal" import { email } from "stores/portal"
email.templates.fetch() email.templates.fetch()
</script> </script>

44
packages/builder/src/pages/builder/portal/manage/users/[userId].svelte

@ -17,7 +17,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { fetchData } from "helpers" import { fetchData } from "helpers"
import { users } from "stores/portal" import { users, auth } from "stores/portal"
import TagsRenderer from "./_components/TagsTableRenderer.svelte" import TagsRenderer from "./_components/TagsTableRenderer.svelte"
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte" import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
@ -56,13 +56,21 @@
let toggleDisabled = false let toggleDisabled = false
async function toggleBuilderAccess({ detail }) { async function toggleFlag(flagName, detail) {
toggleDisabled = true toggleDisabled = true
await users.save({ ...$userFetch?.data, builder: { global: detail } }) await users.save({ ...$userFetch?.data, [flagName]: { global: detail } })
await userFetch.refresh() await userFetch.refresh()
toggleDisabled = false toggleDisabled = false
} }
async function toggleBuilderAccess({ detail }) {
return toggleFlag("builder", detail)
}
async function toggleAdminAccess({ detail }) {
return toggleFlag("admin", detail)
}
async function openUpdateRolesModal({ detail }) { async function openUpdateRolesModal({ detail }) {
selectedApp = detail selectedApp = detail
editRolesModal.show() editRolesModal.show()
@ -107,15 +115,27 @@
<Label size="L">Last name</Label> <Label size="L">Last name</Label>
<Input disabled thin value={$userFetch?.data?.lastName} /> <Input disabled thin value={$userFetch?.data?.lastName} />
</div> </div>
<div class="field"> <!-- don't let a user remove the privileges that let them be here -->
<Label size="L">Development access</Label> {#if userId !== $auth.user._id}
<Toggle <div class="field">
text="" <Label size="L">Development access</Label>
value={$userFetch?.data?.builder?.global} <Toggle
on:change={toggleBuilderAccess} text=""
disabled={toggleDisabled} value={$userFetch?.data?.builder?.global}
/> on:change={toggleBuilderAccess}
</div> disabled={toggleDisabled}
/>
</div>
<div class="field">
<Label size="L">Administration access</Label>
<Toggle
text=""
value={$userFetch?.data?.admin?.global}
on:change={toggleAdminAccess}
disabled={toggleDisabled}
/>
</div>
{/if}
</div> </div>
<div class="regenerate"> <div class="regenerate">
<ActionButton <ActionButton

7
packages/builder/src/pages/builder/portal/manage/users/_layout.svelte

@ -1,7 +0,0 @@
<script>
import { Page } from "@budibase/bbui"
</script>
<Page>
<slot />
</Page>

4
packages/builder/src/pages/builder/portal/manage/users/index.svelte

@ -22,6 +22,7 @@
const schema = { const schema = {
email: {}, email: {},
developmentAccess: { displayName: "Development Access", type: "boolean" }, developmentAccess: { displayName: "Development Access", type: "boolean" },
adminAccess: { displayName: "Admin Access", type: "boolean" },
// role: { type: "options" }, // role: { type: "options" },
group: {}, group: {},
// access: {}, // access: {},
@ -35,7 +36,8 @@
.map(user => ({ .map(user => ({
...user, ...user,
group: ["All users"], group: ["All users"],
developmentAccess: user.builder.global, developmentAccess: !!user.builder?.global,
adminAccess: !!user.admin?.global,
})) }))
let createUserModal let createUserModal

2
packages/builder/src/pages/builder/portal/settings/index.svelte

@ -1,4 +1,4 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
$goto("./general") $goto("./organisation")
</script> </script>

118
packages/builder/src/pages/builder/portal/settings/organisation.svelte

@ -11,10 +11,18 @@
Dropzone, Dropzone,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { organisation } from "stores/portal" import { auth, organisation } from "stores/portal"
import { post } from "builderStore/api" import { post } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { redirect } from "@roxi/routify"
// Only admins allowed here
$: {
if (!$auth.isAdmin) {
$redirect("../../portal")
}
}
const values = writable({ const values = writable({
analytics: !analytics.disabled(), analytics: !analytics.disabled(),
@ -64,68 +72,70 @@
} }
</script> </script>
<Layout> {#if $auth.isAdmin}
<Layout gap="XS" noPadding> <Layout>
<Heading size="M">Organisation</Heading> <Layout gap="XS" noPadding>
<Body> <Heading size="M">Organisation</Heading>
Organisation settings is where you can edit your organisation name and <Body>
logo. You can also configure your platform URL and enable or disable Organisation settings is where you can edit your organisation name and
analytics. logo. You can also configure your platform URL and enable or disable
</Body> analytics.
</Layout> </Body>
<Divider size="S" /> </Layout>
<Layout gap="XS" noPadding> <Divider size="S" />
<Heading size="S">Information</Heading> <Layout gap="XS" noPadding>
<Body size="S">Here you can update your logo and organization name.</Body> <Heading size="S">Information</Heading>
</Layout> <Body size="S">Here you can update your logo and organization name.</Body>
<div class="fields"> </Layout>
<div class="field"> <div class="fields">
<Label size="L">Organization name</Label> <div class="field">
<Input thin bind:value={$values.company} /> <Label size="L">Organization name</Label>
</div> <Input thin bind:value={$values.company} />
<div class="field logo"> </div>
<Label size="L">Logo</Label> <div class="field logo">
<div class="file"> <Label size="L">Logo</Label>
<Dropzone <div class="file">
value={[$values.logo]} <Dropzone
on:change={e => { value={[$values.logo]}
$values.logo = e.detail?.[0] on:change={e => {
}} $values.logo = e.detail?.[0]
/> }}
/>
</div>
</div> </div>
</div> </div>
</div> <Divider size="S" />
<Divider size="S" />
<Layout gap="XS" noPadding>
<Heading size="S">Platform</Heading>
<Body size="S">Here you can set up general platform settings.</Body>
</Layout>
<div class="fields">
<div class="field">
<Label size="L">Platform URL</Label>
<Input thin bind:value={$values.platformUrl} />
</div>
</div>
<Divider size="S" />
<Layout gap="S" noPadding>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading size="S">Analytics</Heading> <Heading size="S">Platform</Heading>
<Body size="S"> <Body size="S">Here you can set up general platform settings.</Body>
If you would like to send analytics that help us make Budibase better,
please let us know below.
</Body>
</Layout> </Layout>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label size="L">Send Analytics to Budibase</Label> <Label size="L">Platform URL</Label>
<Toggle text="" bind:value={$values.analytics} /> <Input thin bind:value={$values.platformUrl} />
</div> </div>
</div> </div>
<Divider size="S" />
<Layout gap="S" noPadding>
<Layout gap="XS" noPadding>
<Heading size="S">Analytics</Heading>
<Body size="S">
If you would like to send analytics that help us make Budibase better,
please let us know below.
</Body>
</Layout>
<div class="fields">
<div class="field">
<Label size="L">Send Analytics to Budibase</Label>
<Toggle text="" bind:value={$values.analytics} />
</div>
</div>
</Layout>
<div>
<Button disabled={loading} on:click={saveConfig} cta>Save</Button>
</div>
</Layout> </Layout>
<div> {/if}
<Button disabled={loading} on:click={saveConfig} cta>Save</Button>
</div>
</Layout>
<style> <style>
.fields { .fields {

10
packages/builder/src/stores/portal/auth.js

@ -5,19 +5,27 @@ export function createAuthStore() {
const user = writable(null) const user = writable(null)
const store = derived(user, $user => { const store = derived(user, $user => {
let initials = null let initials = null
let isAdmin = false
let isBuilder = false
if ($user) { if ($user) {
if ($user.firstName) { if ($user.firstName) {
initials = $user.firstName[0] initials = $user.firstName[0]
if ($user.lastName) { if ($user.lastName) {
initials += $user.lastName[0] initials += $user.lastName[0]
} }
} else { } else if ($user.email) {
initials = $user.email[0] initials = $user.email[0]
} else {
initials = "Unknown"
} }
isAdmin = !!$user.admin?.global
isBuilder = !!$user.builder?.global
} }
return { return {
user: $user, user: $user,
initials, initials,
isAdmin,
isBuilder,
} }
}) })

2
packages/worker/src/api/routes/admin/email.js

@ -2,6 +2,7 @@ const Router = require("@koa/router")
const controller = require("../../controllers/admin/email") const controller = require("../../controllers/admin/email")
const { EmailTemplatePurpose } = require("../../../constants") const { EmailTemplatePurpose } = require("../../../constants")
const joiValidator = require("../../../middleware/joi-validator") const joiValidator = require("../../../middleware/joi-validator")
const adminOnly = require("../../../middleware/adminOnly")
const Joi = require("joi") const Joi = require("joi")
const router = Router() const router = Router()
@ -21,6 +22,7 @@ function buildEmailSendValidation() {
router.post( router.post(
"/api/admin/email/send", "/api/admin/email/send",
buildEmailSendValidation(), buildEmailSendValidation(),
adminOnly,
controller.sendEmail controller.sendEmail
) )

20
packages/worker/src/api/routes/admin/users.js

@ -54,16 +54,8 @@ router
buildUserSaveValidation(), buildUserSaveValidation(),
controller.save controller.save
) )
.get("/api/admin/users", controller.fetch) .get("/api/admin/users", adminOnly, controller.fetch)
.post("/api/admin/users/init", controller.adminUser)
.get("/api/admin/users/self", controller.getSelf)
.post(
"/api/admin/users/self",
buildUserSaveValidation(true),
controller.updateSelf
)
.delete("/api/admin/users/:id", adminOnly, controller.destroy) .delete("/api/admin/users/:id", adminOnly, controller.destroy)
.get("/api/admin/users/:id", controller.find)
.get("/api/admin/roles/:appId") .get("/api/admin/roles/:appId")
.post( .post(
"/api/admin/users/invite", "/api/admin/users/invite",
@ -71,10 +63,20 @@ router
buildInviteValidation(), buildInviteValidation(),
controller.invite controller.invite
) )
// non-admin endpoints
.post(
"/api/admin/users/self",
buildUserSaveValidation(true),
controller.updateSelf
)
.post( .post(
"/api/admin/users/invite/accept", "/api/admin/users/invite/accept",
buildInviteAcceptValidation(), buildInviteAcceptValidation(),
controller.inviteAccept controller.inviteAccept
) )
.post("/api/admin/users/init", controller.adminUser)
.get("/api/admin/users/self", controller.getSelf)
// admin endpoint but needs to come at end (blocks other endpoints otherwise)
.get("/api/admin/users/:id", adminOnly, controller.find)
module.exports = router module.exports = router

5
packages/worker/src/middleware/adminOnly.js

@ -1,5 +1,8 @@
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
if (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) { if (
!ctx.internal &&
(!ctx.user || !ctx.user.admin || !ctx.user.admin.global)
) {
ctx.throw(403, "Admin user only endpoint.") ctx.throw(403, "Admin user only endpoint.")
} }
return next() return next()

Loading…
Cancel
Save