From 43000075c07bf217e2ac6539cd5f0c2eebbd8afa Mon Sep 17 00:00:00 2001 From: yehong <58927640+yehong-z@users.noreply.github.com> Date: Thu, 31 Aug 2023 23:53:25 +0800 Subject: [PATCH] feat: demo mode (#616) --- conf/app.conf | 1 + controllers/util.go | 22 +++++++++++++ main.go | 1 + routers/filter.go | 38 ++++++++++++++++++++++ web/src/Conf.js | 1 + web/src/Setting.js | 4 +++ web/src/backend/FetchFilter.js | 59 ++++++++++++++++++++++++++++++++++ web/src/index.js | 1 + 8 files changed, 127 insertions(+) create mode 100644 web/src/backend/FetchFilter.js diff --git a/conf/app.conf b/conf/app.conf index 019827f..f89c64b 100644 --- a/conf/app.conf +++ b/conf/app.conf @@ -7,6 +7,7 @@ driverName = mysql dataSourceName = root:123@tcp(localhost:3306)/ dbName = casibase redisEndpoint = +isDemoMode = false landingFolder = casibase-landing casdoorEndpoint = http://localhost:8000 clientId = af6b5aa958822fb9dc33 diff --git a/controllers/util.go b/controllers/util.go index 7a77bdd..b6a19b1 100644 --- a/controllers/util.go +++ b/controllers/util.go @@ -14,6 +14,8 @@ package controllers +import "github.com/astaxie/beego/context" + type Response struct { Status string `json:"status"` Msg string `json:"msg"` @@ -65,3 +67,23 @@ func (c *ApiController) RequireAdmin() bool { return false } + +func DenyRequest(ctx *context.Context) { + responseError(ctx, "Unauthorized operation") +} + +func responseError(ctx *context.Context, error string, data ...interface{}) { + resp := Response{Status: "error", Msg: error} + switch len(data) { + case 2: + resp.Data2 = data[1] + fallthrough + case 1: + resp.Data = data[0] + } + + err := ctx.Output.JSON(resp, true, false) + if err != nil { + panic(err) + } +} diff --git a/main.go b/main.go index 39b0bf7..3066540 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,7 @@ func main() { // https://studygolang.com/articles/2303 beego.InsertFilter("/", beego.BeforeRouter, routers.TransparentStatic) // must has this for default page beego.InsertFilter("/*", beego.BeforeRouter, routers.TransparentStatic) + beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter) if beego.AppConfig.String("redisEndpoint") == "" { beego.BConfig.WebConfig.Session.SessionProvider = "file" diff --git a/routers/filter.go b/routers/filter.go index 7252c71..c34c5be 100644 --- a/routers/filter.go +++ b/routers/filter.go @@ -19,6 +19,9 @@ import ( "net/http" "strings" + "github.com/casbin/casibase/conf" + "github.com/casbin/casibase/controllers" + "github.com/astaxie/beego" "github.com/astaxie/beego/context" "github.com/casbin/casibase/util" @@ -57,3 +60,38 @@ func TransparentStatic(ctx *context.Context) { http.ServeFile(ctx.ResponseWriter, ctx.Request, "web/build/index.html") } } + +func ApiFilter(ctx *context.Context) { + method := ctx.Request.Method + urlPath := getUrlPath(ctx.Request.URL.Path) + + if conf.IsDemoMode() { + if !isAllowedInDemoMode(method, urlPath) { + controllers.DenyRequest(ctx) + } + } +} + +func getUrlPath(urlPath string) string { + if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) { + return "/cas" + } + + if strings.HasPrefix(urlPath, "/api/login/oauth") { + return "/api/login/oauth" + } + + if strings.HasPrefix(urlPath, "/api/webauthn") { + return "/api/webauthn" + } + + return urlPath +} + +func isAllowedInDemoMode(method string, urlPath string) bool { + if method == "POST" && !(strings.HasPrefix(urlPath, "/api/signin") || urlPath == "/api/signout") { + return false + } + + return true +} diff --git a/web/src/Conf.js b/web/src/Conf.js index f78a762..b40c025 100644 --- a/web/src/Conf.js +++ b/web/src/Conf.js @@ -30,6 +30,7 @@ export const ForceLanguage = ""; export const DefaultLanguage = "en"; export const AppUrl = ""; +export const IsDemoMode = false; export const ThemeDefault = { themeType: "default", diff --git a/web/src/Setting.js b/web/src/Setting.js index 900397b..54f61d3 100644 --- a/web/src/Setting.js +++ b/web/src/Setting.js @@ -648,3 +648,7 @@ export function renderExternalLink() { ); } + +export function isResponseDenied(data) { + return data.msg === "Unauthorized operation"; +} diff --git a/web/src/backend/FetchFilter.js b/web/src/backend/FetchFilter.js new file mode 100644 index 0000000..20f62e8 --- /dev/null +++ b/web/src/backend/FetchFilter.js @@ -0,0 +1,59 @@ +// Copyright 2023 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {Modal} from "antd"; +import {ExclamationCircleFilled} from "@ant-design/icons"; +import i18next from "i18next"; +import * as Conf from "../Conf"; +import * as Setting from "../Setting"; + +const {confirm} = Modal; +const {fetch: originalFetch} = window; + +const demoModeCallback = (res) => { + res.json().then(data => { + if (Setting.isResponseDenied(data)) { + confirm({ + title: i18next.t("general:This is a read-only demo site!"), + icon: , + content: i18next.t("general:Go to writable demo site?"), + okText: i18next.t("general:OK"), + cancelText: i18next.t("general:Cancel"), + onOk() { + Setting.openLink(`https://demo.ai.casbin.com/${location.pathname}${location.search}?username=built-in/admin&password=123`); + }, + onCancel() {}, + }); + } + }); +}; + +const requestFilters = []; +const responseFilters = []; + +if (Conf.IsDemoMode) { + responseFilters.push(demoModeCallback); +} + +window.fetch = async(url, option = {}) => { + requestFilters.forEach(filter => filter(url, option)); + return new Promise((resolve, reject) => { + originalFetch(url, option).then(res => { + // eslint-disable-next-line no-console + console.log(res.clone()); + responseFilters.forEach(filter => filter(res.clone())); + resolve(res); + }); + }); +}; diff --git a/web/src/index.js b/web/src/index.js index 1d41e75..3f23fcd 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -28,6 +28,7 @@ import * as serviceWorker from "./serviceWorker"; // import 'antd/dist/antd.min.css'; import {BrowserRouter} from "react-router-dom"; import "./i18n"; +import "./backend/FetchFilter"; const container = document.getElementById("root");