Merge branch 'feature/global-user-management' of github.com:Budibase/budibase into feature/global-user-management

This commit is contained in:
mike12345567 2021-04-12 11:09:22 +01:00
commit a2c7ea9ba4
22 changed files with 259 additions and 217 deletions

View File

@ -26,7 +26,7 @@ static_resources:
cluster: redis-service cluster: redis-service
prefix_rewrite: "/" prefix_rewrite: "/"
- match: { prefix: "/api/admin" } - match: { prefix: "/api/admin/" }
route: route:
cluster: worker-dev cluster: worker-dev

View File

@ -1,125 +0,0 @@
static_resources:
listeners:
- name: main_listener
address:
socket_address: { address: 0.0.0.0, port_value: 10000 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress
codec_type: auto
route_config:
name: local_route
virtual_hosts:
- name: local_services
domains: ["*"]
routes:
- match: { prefix: "/db/" }
route:
cluster: couchdb-service
prefix_rewrite: "/"
- match: { prefix: "/cache/" }
route:
cluster: redis-service
prefix_rewrite: "/"
- match: { prefix: "/api/" }
route:
cluster: server-dev
- match: { prefix: "/app_" }
route:
cluster: server-dev
- match: { prefix: "/builder/" }
route:
cluster: builder-dev
- match: { prefix: "/builder" }
route:
cluster: builder-dev
prefix_rewrite: "/builder/"
# minio is on the default route because this works
# best, minio + AWS SDK doesn't handle path proxy
- match: { prefix: "/" }
route:
cluster: minio-service
http_filters:
- name: envoy.filters.http.router
clusters:
- name: minio-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: minio-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: minio-service
port_value: 9000
- name: couchdb-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: couchdb-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: couchdb-service
port_value: 5984
- name: redis-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: redis-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: redis-service
port_value: 6379
- name: server-dev
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: server-dev
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.docker.internal
port_value: 4001
- name: builder-dev
connect_timeout: 15s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: builder-dev
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.docker.internal
port_value: 3000

View File

@ -4,7 +4,7 @@ const JwtStrategy = require("passport-jwt").Strategy
// const GoogleStrategy = require("passport-google-oauth").Strategy // const GoogleStrategy = require("passport-google-oauth").Strategy
const CouchDB = require("./db") const CouchDB = require("./db")
const { StaticDatabases } = require("./db/utils") const { StaticDatabases } = require("./db/utils")
const { jwt, local, google } = require("./middleware") const { jwt, local, google, authenticated } = require("./middleware")
const { Cookies, UserStatus } = require("./constants") const { Cookies, UserStatus } = require("./constants")
const { hash, compare } = require("./hashing") const { hash, compare } = require("./hashing")
const { getAppId, setCookie } = require("./utils") const { getAppId, setCookie } = require("./utils")
@ -45,4 +45,5 @@ module.exports = {
compare, compare,
getAppId, getAppId,
setCookie, setCookie,
authenticated,
} }

View File

@ -0,0 +1,53 @@
const CouchDB = require("../db")
const { Cookies } = require("../constants")
const { getAppId, setCookie, getCookie } = require("../utils")
const { StaticDatabases } = require("../db/utils")
async function setCurrentAppContext(ctx) {
let role = "PUBLIC"
// Current app cookie
let appId = getAppId(ctx)
if (!appId) {
ctx.user = {
role,
}
return
}
const currentAppCookie = getCookie(ctx, Cookies.CurrentApp, { decrypt: true })
const appIdChanged = appId && currentAppCookie.appId !== appId
if (appIdChanged) {
try {
// get roles for user from global DB
const db = new CouchDB(StaticDatabases.USER)
const user = await db.get(ctx.user)
role = user.roles[appId]
} catch (err) {
// no user exists
}
} else if (currentAppCookie.appId) {
appId = currentAppCookie.appId
}
setCookie(ctx, { appId, role }, Cookies.CurrentApp, { encrypt: true })
return appId
}
module.exports = async (ctx, next) => {
try {
// check the actual user is authenticated first
const authCookie = getCookie(ctx, Cookies.Auth, { decrypt: true })
if (authCookie) {
ctx.isAuthenticated = true
ctx.user = authCookie._id
}
ctx.appId = await setCurrentAppContext(ctx)
await next()
} catch (err) {
console.log(err)
ctx.throw(err.status || 403, err.text)
}
}

View File

@ -1,9 +1,11 @@
const jwt = require("./passport/jwt") const jwt = require("./passport/jwt")
const local = require("./passport/local") const local = require("./passport/local")
const google = require("./passport/google") const google = require("./passport/google")
const authenticated = require("./authenticated")
module.exports = { module.exports = {
google, google,
jwt, jwt,
local, local,
authenticated,
} }

View File

@ -1,4 +1,6 @@
const { DocumentTypes, SEPARATOR } = require("./db/utils") const { DocumentTypes, SEPARATOR } = require("./db/utils")
const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -39,6 +41,23 @@ exports.getAppId = ctx => {
return appId return appId
} }
/**
* Get a cookie from context, and decrypt if necessary.
* @param {object} ctx The request which is to be manipulated.
* @param {string} name The name of the cookie to get.
* @param {object} options options .
*/
exports.getCookie = (ctx, value, options = {}) => {
const cookie = ctx.cookies.get(value)
if (!cookie) return
if (!options.decrypt) return cookie
const payload = jwt.verify(cookie, process.env.JWT_SECRET)
return payload
}
/** /**
* Store a cookie for the request, has a hardcoded expiry. * Store a cookie for the request, has a hardcoded expiry.
* @param {object} ctx The request which is to be manipulated. * @param {object} ctx The request which is to be manipulated.
@ -52,6 +71,11 @@ exports.setCookie = (ctx, value, name = "builder") => {
if (!value) { if (!value) {
ctx.cookies.set(name) ctx.cookies.set(name)
} else { } else {
if (options.encrypt) {
value = jwt.sign(value, process.env.JWT_SECRET, {
expiresIn: "1 day",
})
}
ctx.cookies.set(name, value, { ctx.cookies.set(name, value, {
expires, expires,
path: "/", path: "/",

View File

@ -20,9 +20,9 @@ export const get = apiCall("GET")
export const patch = apiCall("PATCH") export const patch = apiCall("PATCH")
export const del = apiCall("DELETE") export const del = apiCall("DELETE")
export const put = apiCall("PUT") export const put = apiCall("PUT")
export const getBuilderCookie = async () => { // export const getBuilderCookie = async () => {
await post("/api/builder/login", {}) // await post("/api/builder/login", {})
} // }
export default { export default {
post: apiCall("POST"), post: apiCall("POST"),
@ -30,5 +30,5 @@ export default {
patch: apiCall("PATCH"), patch: apiCall("PATCH"),
delete: apiCall("DELETE"), delete: apiCall("DELETE"),
put: apiCall("PUT"), put: apiCall("PUT"),
getBuilderCookie, // getBuilderCookie,
} }

View File

@ -6,7 +6,7 @@ import { derived, writable } from "svelte/store"
import analytics from "analytics" import analytics from "analytics"
import { FrontendTypes, LAYOUT_NAMES } from "../constants" import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { findComponent } from "./storeUtils" import { findComponent } from "./storeUtils"
import { getBuilderCookie } from "./api" // import { getBuilderCookie } from "./api"
export const store = getFrontendStore() export const store = getFrontendStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()
@ -59,7 +59,7 @@ export const selectedAccessRole = writable("BASIC")
export const initialise = async () => { export const initialise = async () => {
try { try {
// TODO this needs to be replaced by a real login // TODO this needs to be replaced by a real login
await getBuilderCookie() // await getBuilderCookie()
await analytics.activate() await analytics.activate()
analytics.captureEvent("Builder Started") analytics.captureEvent("Builder Started")
} catch (err) { } catch (err) {

View File

@ -0,0 +1,52 @@
<script>
import { Button, Label, Input, TextArea, Spacer } from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import { auth } from "stores/backend"
let username = ""
let password = ""
async function login() {
try {
const json = await auth.login({
username,
password,
})
notifier.success("Logged in successfully.")
} catch (err) {
console.error(err)
notifier.danger(`Error logging in: ${err}`)
}
}
async function createTestUser() {
try {
const json = await auth.createUser({
email: "test@test.com",
password: "test",
roles: {},
})
notifier.success("Test user created")
} catch (err) {
console.error(err)
}
}
</script>
<form on:submit|preventDefault>
<Spacer large />
<Label small>Email</Label>
<Input outline bind:value={username} />
<Spacer large />
<Label small>Password</Label>
<Input outline type="password" on:change bind:value={password} />
<Spacer large />
<Button primary on:click={login}>Login</Button>
<Button secondary on:click={createTestUser}>Create Test User</Button>
</form>
<style>
form {
width: 60%;
}
</style>

View File

@ -0,0 +1 @@
export { LoginForm } from "./LoginForm.svelte"

View File

@ -7,15 +7,20 @@
CommunityIcon, CommunityIcon,
BugIcon, BugIcon,
} from "components/common/Icons" } from "components/common/Icons"
import LoginForm from "components/login/LoginForm.svelte"
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte" import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
import Logo from "/assets/budibase-logo.svg" import Logo from "/assets/budibase-logo.svg"
import { auth } from "stores/backend"
let modal let modal
</script> </script>
<div class="root"> {#if $auth.user}
<div class="root">
<div class="ui-nav"> <div class="ui-nav">
<div class="home-logo"><img src={Logo} alt="Budibase icon" /></div> <div class="home-logo">
<img src={Logo} alt="Budibase icon" />
</div>
<div class="nav-section"> <div class="nav-section">
<div class="nav-top"> <div class="nav-top">
<Link icon={AppsIcon} title="Apps" href="/" active /> <Link icon={AppsIcon} title="Apps" href="/" active />
@ -44,7 +49,12 @@
<div class="main"> <div class="main">
<slot /> <slot />
</div> </div>
</div> </div>
{:else}
<section class="login">
<LoginForm />
</section>
{/if}
<style> <style>
.root { .root {
@ -55,6 +65,14 @@
background: var(--grey-1); background: var(--grey-1);
} }
.login {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.main { .main {
grid-column: 2; grid-column: 2;
overflow: auto; overflow: auto;

View File

@ -0,0 +1,32 @@
import { writable, get } from "svelte/store"
import api from "../../builderStore/api"
export function createAuthStore() {
const { subscribe, set } = writable({})
const user = localStorage.getItem("auth:user")
if (user) set({ user: JSON.parse(user) })
return {
subscribe,
login: async creds => {
const response = await api.post(`/api/admin/auth`, creds)
const json = await response.json()
if (json.user) {
localStorage.setItem("auth:user", JSON.stringify(json.user))
set({ user: json.user })
}
},
logout: async () => {
const response = await api.post(`/api/auth/logout`)
const json = await response.json()
set({ user: false })
},
createUser: async user => {
const response = await api.post(`/api/admin/users`, user)
const json = await response.json()
},
}
}
export const auth = createAuthStore()

View File

@ -7,3 +7,4 @@ export { roles } from "./roles"
export { datasources } from "./datasources" export { datasources } from "./datasources"
export { integrations } from "./integrations" export { integrations } from "./integrations"
export { queries } from "./queries" export { queries } from "./queries"
export { auth } from "./auth"

View File

@ -70,10 +70,10 @@ exports.authenticate = async ctx => {
} }
} }
exports.builderLogin = async ctx => { // exports.builderLogin = async ctx => {
await setBuilderToken(ctx) // await setBuilderToken(ctx)
ctx.status = 200 // ctx.status = 200
} // }
exports.fetchSelf = async ctx => { exports.fetchSelf = async ctx => {
const { userId, appId } = ctx.user const { userId, appId } = ctx.user

View File

@ -1,5 +1,5 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const authenticated = require("../middleware/authenticated") const { authenticated } = require("@budibase/auth")
const compress = require("koa-compress") const compress = require("koa-compress")
const zlib = require("zlib") const zlib = require("zlib")
const { mainRoutes, authRoutes, staticRoutes } = require("./routes") const { mainRoutes, authRoutes, staticRoutes } = require("./routes")

View File

@ -7,7 +7,7 @@ const router = Router()
router.post("/api/authenticate", controller.authenticate) router.post("/api/authenticate", controller.authenticate)
// TODO: this is a hack simply to make sure builder has a cookie until auth reworked // TODO: this is a hack simply to make sure builder has a cookie until auth reworked
router.post("/api/builder/login", authorized(BUILDER), controller.builderLogin) // router.post("/api/builder/login", authorized(BUILDER), controller.builderLogin)
// doesn't need authorization as can only fetch info about self // doesn't need authorization as can only fetch info about self
router.get("/api/self", controller.fetchSelf) router.get("/api/self", controller.fetchSelf)

View File

@ -40,7 +40,8 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
const role = ctx.user.role const role = ctx.user.role
const isAdmin = ADMIN_ROLES.includes(role._id) const isAdmin = ADMIN_ROLES.includes(role._id)
const isAuthed = ctx.auth.authenticated // const isAuthed = ctx.auth.authenticated
const isAuthed = ctx.isAuthenticated
const { basePermissions, permissions } = await getUserPermissions( const { basePermissions, permissions } = await getUserPermissions(
ctx.appId, ctx.appId,
@ -50,11 +51,11 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
// this may need to change in the future, right now only admins // this may need to change in the future, right now only admins
// can have access to builder features, this is hard coded into // can have access to builder features, this is hard coded into
// our rules // our rules
if (isAdmin && isAuthed) { // if (isAdmin && isAuthed) {
return next() // return next()
} else if (permType === PermissionTypes.BUILDER) { // } else if (permType === PermissionTypes.BUILDER) {
return ctx.throw(403, "Not Authorized") // return ctx.throw(403, "Not Authorized")
} // }
if ( if (
hasResource(ctx) && hasResource(ctx) &&

View File

@ -1,4 +1,4 @@
const { passport } = require("@budibase/auth") const { passport, Cookies } = require("@budibase/auth")
exports.authenticate = async (ctx, next) => { exports.authenticate = async (ctx, next) => {
return passport.authenticate("local", async (err, user) => { return passport.authenticate("local", async (err, user) => {
@ -9,13 +9,24 @@ exports.authenticate = async (ctx, next) => {
const expires = new Date() const expires = new Date()
expires.setDate(expires.getDate() + 1) expires.setDate(expires.getDate() + 1)
ctx.cookies.set("budibase:auth", user.token, { if (!user) {
ctx.body = { success: false, user }
return
}
ctx.cookies.set(Cookies.Auth, user.token, {
expires, expires,
path: "/", path: "/",
httpOnly: false, httpOnly: false,
overwrite: true, overwrite: true,
}) })
ctx.body = { success: true } delete user.token
ctx.body = { success: true, user }
})(ctx, next) })(ctx, next)
} }
exports.googleAuth = async (ctx, next) => {
// return passport.authenticate("google")
}

View File

@ -1,7 +1,7 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../../controllers/admin") const controller = require("../../controllers/admin")
const authenticated = require("../../../middleware/authenticated")
const joiValidator = require("../../../middleware/joi-validator") const joiValidator = require("../../../middleware/joi-validator")
const { authenticated } = require("@budibase/auth")
const Joi = require("joi") const Joi = require("joi")
const router = Router() const router = Router()

View File

@ -1,8 +1,18 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const { passport } = require("@budibase/auth")
const authController = require("../controllers/auth") const authController = require("../controllers/auth")
const router = Router() const router = Router()
router.post("/api/auth/authenticate", authController.authenticate) router
.post("/api/admin/auth", authController.authenticate)
.get("/api/auth/google", passport.authenticate("google"))
.get(
"/api/auth/google/callback",
passport.authenticate("google", {
successRedirect: "/app",
failureRedirect: "/",
})
)
module.exports = router module.exports = router

View File

@ -1,32 +0,0 @@
const { passport, getAppId, setCookie, Cookies } = require("@budibase/auth")
module.exports = async (ctx, next) => {
// do everything we can to make sure the appId is held correctly
let appId = getAppId(ctx)
const cookieAppId = ctx.cookies.get(Cookies.CurrentApp)
// const builtinRoles = getBuiltinRoles()
if (appId && cookieAppId !== appId) {
setCookie(ctx, appId, Cookies.CurrentApp)
} else if (cookieAppId) {
appId = cookieAppId
}
return next()
// return passport.authenticate("jwt", async (err, user) => {
// if (err) {
// return ctx.throw(err.status || 403, err)
// }
//
// try {
// ctx.appId = appId
// ctx.isAuthenticated = true
// // TODO: introduce roles again
// ctx.user = user
// await next()
// } catch (err) {
// console.log(err)
// ctx.throw(err.status || 403, err.text)
// }
// })(ctx, next)
}

View File

@ -1,7 +0,0 @@
/**
* Check the user token, used when creating admin resources, like for example
* a global user record.
*/
module.exports = async (ctx, next) => {
return next()
}