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

Locking down administration endpoints and routes
This commit is contained in:
Michael Drury 2021-05-24 13:49:22 +01:00 committed by GitHub
commit 0b728f07fa
14 changed files with 181 additions and 106 deletions

View File

@ -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)

View File

@ -20,9 +20,17 @@
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 => {
let menu = [{ title: "Apps", href: "/builder/portal/apps" }]
if (admin) {
menu = menu.concat([
{
title: "Users",
href: "/builder/portal/manage/users",
heading: "Manage",
},
{ title: "Auth", href: "/builder/portal/manage/auth" }, { title: "Auth", href: "/builder/portal/manage/auth" },
{ title: "Email", href: "/builder/portal/manage/email" }, { title: "Email", href: "/builder/portal/manage/email" },
{ {
@ -30,8 +38,22 @@
href: "/builder/portal/settings/organisation", href: "/builder/portal/settings/organisation",
heading: "Settings", heading: "Settings",
}, },
{ title: "Theming", href: "/builder/portal/settings/theming" }, {
] 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

View File

@ -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}

View File

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

View File

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

View File

@ -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,6 +115,8 @@
<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>
<!-- don't let a user remove the privileges that let them be here -->
{#if userId !== $auth.user._id}
<div class="field"> <div class="field">
<Label size="L">Development access</Label> <Label size="L">Development access</Label>
<Toggle <Toggle
@ -116,6 +126,16 @@
disabled={toggleDisabled} disabled={toggleDisabled}
/> />
</div> </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

View File

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

View File

@ -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

View File

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

View File

@ -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,7 +72,8 @@
} }
</script> </script>
<Layout> {#if $auth.isAdmin}
<Layout>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading size="M">Organisation</Heading> <Heading size="M">Organisation</Heading>
<Body> <Body>
@ -125,7 +134,8 @@
<div> <div>
<Button disabled={loading} on:click={saveConfig} cta>Save</Button> <Button disabled={loading} on:click={saveConfig} cta>Save</Button>
</div> </div>
</Layout> </Layout>
{/if}
<style> <style>
.fields { .fields {

View File

@ -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,
} }
}) })

View File

@ -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
) )

View File

@ -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

View File

@ -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()