Explorar o código

Disable some menu items in read-only mode (#11733)

* :art: kernel supports read-only publishing services

* :bug: Fix authentication vulnerabilities

* :art: Protect secret information

* :art: Adjust the permission control

* :art: Adjust the permission control

* :art: Fixed the vulnerability that `getFile` gets file `conf.json`

* :art: Add API `/api/setting/setPublish`

* :art: Add API `/api/setting/getPublish`

* :bug: Fixed the issue that PWA-related files could not pass BasicAuth

* :art: Add a settings panel for publishing features

* :memo: Add guide for `Publish Service`

* :memo: Update Japanese user guide

* :art: Merge fixed static file services

* :art: Disable some menu items in read-only mode

* :art: Disable some menu items in read-only mode

* Update router.go
Yingyi / 颖逸 hai 1 ano
pai
achega
f25b36ff38

+ 6 - 4
app/src/config/editor.ts

@@ -310,10 +310,12 @@ export const editor = {
         if (fontFamilyElement.tagName === "SELECT") {
             let fontFamilyHTML = `<option value="">${window.siyuan.languages.default}</option>`;
             fetchPost("/api/system/getSysFonts", {}, (response) => {
-                response.data.forEach((item: string) => {
-                    fontFamilyHTML += `<option value="${item}"${window.siyuan.config.editor.fontFamily === item ? " selected" : ""}>${item}</option>`;
-                });
-                fontFamilyElement.innerHTML = fontFamilyHTML;
+                if (response.code === 0) {
+                    response.data.forEach((item: string) => {
+                        fontFamilyHTML += `<option value="${item}"${window.siyuan.config.editor.fontFamily === item ? " selected" : ""}>${item}</option>`;
+                    });
+                    fontFamilyElement.innerHTML = fontFamilyHTML;
+                }
             });
         }
         editor.element.querySelector("#clearHistory").addEventListener("click", () => {

+ 32 - 14
app/src/index.ts

@@ -38,6 +38,8 @@ export class App {
         registerServiceWorker(`${Constants.SERVICE_WORKER_PATH}?v=${Constants.SIYUAN_VERSION}`);
         /// #endif
         addBaseURL();
+        addScriptSync(`${Constants.PROTYLE_CDN}/js/lute/lute.min.js?v=${Constants.SIYUAN_VERSION}`, "protyleLuteScript"),
+        addScript(`${Constants.PROTYLE_CDN}/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}`, "protyleWcHtmlScript"),
 
         this.appId = Constants.SIYUAN_APPID;
         window.siyuan = {
@@ -158,24 +160,40 @@ export class App {
         };
 
         fetchPost("/api/system/getConf", {}, async (response) => {
-            addScriptSync(`${Constants.PROTYLE_CDN}/js/lute/lute.min.js?v=${Constants.SIYUAN_VERSION}`, "protyleLuteScript");
-            addScript(`${Constants.PROTYLE_CDN}/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}`, "protyleWcHtmlScript");
             window.siyuan.config = response.data.conf;
-            await loadPlugins(this);
-            getLocalStorage(() => {
-                fetchGet(`/appearance/langs/${window.siyuan.config.appearance.lang}.json?v=${Constants.SIYUAN_VERSION}`, (lauguages: IObject) => {
-                    window.siyuan.languages = lauguages;
-                    window.siyuan.menus = new Menus(this);
-                    bootSync();
+
+            const promises = [
+                loadPlugins(this),
+                new Promise<void>(resolve => getLocalStorage(resolve)),
+                new Promise<void>(resolve => fetchGet(
+                    `/appearance/langs/${window.siyuan.config.appearance.lang}.json?v=${Constants.SIYUAN_VERSION}`,
+                    (lauguages: IObject) => {
+                        window.siyuan.languages = lauguages;
+                        resolve();
+                    },
+                )),
+            ];
+
+            if (!window.siyuan.config.readonly) {
+                promises.push(new Promise<void>(resolve => {
                     fetchPost("/api/setting/getCloudUser", {}, userResponse => {
                         window.siyuan.user = userResponse.data;
-                        onGetConfig(response.data.start, this);
-                        account.onSetaccount();
-                        setTitle(window.siyuan.languages.siyuanNote);
-                        initMessage();
+                        resolve();
                     });
-                });
-            });
+                }));
+            }
+
+            await Promise.all(promises);
+
+            if (!window.siyuan.config.readonly) {
+                bootSync();
+            }
+
+            window.siyuan.menus = new Menus(this);
+            onGetConfig(response.data.start, this);
+            account.onSetaccount();
+            setTitle(window.siyuan.languages.siyuanNote);
+            initMessage();
         });
         setNoteBook();
         initBlockPopover(this);

+ 1 - 1
app/src/layout/Wnd.ts

@@ -105,7 +105,7 @@ export class Wnd {
         this.headersElement.parentElement.addEventListener("click", (event) => {
             let target = event.target as HTMLElement;
             while (target && !target.isEqualNode(this.headersElement)) {
-                if (target.classList.contains("block__icon") && target.getAttribute("data-type") === "new") {
+                if (target.classList.contains("block__icon") && target.getAttribute("data-type") === "new" && !window.siyuan.config.readonly) {
                     setPanelFocus(this.headersElement.parentElement.parentElement);
                     newFile({
                         app,

+ 1 - 0
app/src/layout/status.ts

@@ -68,6 +68,7 @@ export const initStatus = (isWindow = false) => {
                     window.siyuan.menus.menu.append(new MenuItem({
                         label: window.siyuan.languages.userGuide,
                         icon: "iconHelp",
+                        disabled: window.siyuan.config.readonly,
                         click: () => {
                             mountHelp();
                         }

+ 2 - 1
app/src/layout/topBar.ts

@@ -305,6 +305,7 @@ const openPlugin = (app: App, target: Element) => {
         menu.addItem({
             icon: "iconSettings",
             label: window.siyuan.languages.manage,
+            disabled: window.siyuan.config.readonly,
             click() {
                 openSetting(app).element.querySelector('.b3-tab-bar [data-name="bazaar"]').dispatchEvent(new CustomEvent("click"));
             }
@@ -374,7 +375,7 @@ const openPlugin = (app: App, target: Element) => {
         }
     });
     if (!hasPlugin) {
-        window.siyuan.menus.menu.element.querySelector(".b3-menu__separator").remove();
+        window.siyuan.menus.menu.element.querySelector(".b3-menu__separator")?.remove();
     }
     let rect = target.getBoundingClientRect();
     if (rect.width === 0) {

+ 3 - 1
app/src/menus/commonMenuItem.ts

@@ -448,6 +448,7 @@ export const exportMd = (id: string) => {
             label: window.siyuan.languages.template,
             iconClass: "ft__error",
             icon: "iconMarkdown",
+            disabled: window.siyuan.config.readonly,
             click: async () => {
                 const result = await fetchSyncPost("/api/block/getRefText", {id: id});
 
@@ -507,8 +508,9 @@ export const exportMd = (id: string) => {
                                 });
                             });
                             return;
+                        } else if (response.code === 0) {
+                            showMessage(window.siyuan.languages.exportTplSucc);
                         }
-                        showMessage(window.siyuan.languages.exportTplSucc);
                     });
                     dialog.destroy();
                 });

+ 1 - 0
app/src/menus/workspace.ts

@@ -440,6 +440,7 @@ export const workspaceMenu = (app: App, rect: DOMRect) => {
             window.siyuan.menus.menu.append(new MenuItem({
                 label: window.siyuan.languages.userGuide,
                 icon: "iconHelp",
+                disabled: window.siyuan.config.readonly,
                 click: () => {
                     mountHelp();
                 }

+ 48 - 25
app/src/mobile/index.ts

@@ -37,9 +37,10 @@ class App {
         if (!window.webkit?.messageHandlers && !window.JSAndroid) {
             registerServiceWorker(`${Constants.SERVICE_WORKER_PATH}?v=${Constants.SIYUAN_VERSION}`);
         }
+        addBaseURL();
         addScriptSync(`${Constants.PROTYLE_CDN}/js/lute/lute.min.js?v=${Constants.SIYUAN_VERSION}`, "protyleLuteScript");
         addScript(`${Constants.PROTYLE_CDN}/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}`, "protyleWcHtmlScript");
-        addBaseURL();
+
         this.appId = Constants.SIYUAN_APPID;
         window.siyuan = {
             zIndex: 10,
@@ -89,30 +90,7 @@ class App {
         fetchPost("/api/system/getConf", {}, async (confResponse) => {
             window.siyuan.config = confResponse.data.conf;
             correctHotkey(siyuanApp);
-            await loadPlugins(this);
-            getLocalStorage(() => {
-                fetchGet(`/appearance/langs/${window.siyuan.config.appearance.lang}.json?v=${Constants.SIYUAN_VERSION}`, (lauguages: IObject) => {
-                    window.siyuan.languages = lauguages;
-                    window.siyuan.menus = new Menus(this);
-                    document.title = window.siyuan.languages.siyuanNote;
-                    bootSync();
-                    loadAssets(confResponse.data.conf.appearance);
-                    initMessage();
-                    initAssets();
-                    fetchPost("/api/setting/getCloudUser", {}, userResponse => {
-                        window.siyuan.user = userResponse.data;
-                        fetchPost("/api/system/getEmojiConf", {}, emojiResponse => {
-                            window.siyuan.emojis = emojiResponse.data as IEmoji[];
-                            setNoteBook(() => {
-                                initFramework(this, confResponse.data.start);
-                                initRightMenu(this);
-                                openChangelog();
-                            });
-                        });
-                    });
-                    addGA();
-                });
-            });
+
             document.addEventListener("touchstart", handleTouchStart, false);
             document.addEventListener("touchmove", handleTouchMove, false);
             document.addEventListener("touchend", (event) => {
@@ -140,6 +118,51 @@ class App {
                     }
                 }
             });
+
+            const promises = [
+                loadPlugins(this),
+                new Promise<void>(resolve => getLocalStorage(resolve)),
+                new Promise<void>(resolve => fetchGet(
+                    `/appearance/langs/${window.siyuan.config.appearance.lang}.json?v=${Constants.SIYUAN_VERSION}`,
+                    (lauguages: IObject) => {
+                        window.siyuan.languages = lauguages;
+                        resolve();
+                    },
+                )),
+                new Promise<void>(resolve => {
+                    fetchPost("/api/setting/getEmojiConf", {}, emojiResponse => {
+                        window.siyuan.emojis = emojiResponse.data as IEmoji[];
+                        resolve();
+                    });
+                }),
+            ];
+
+            if (!window.siyuan.config.readonly) {
+                promises.push(new Promise<void>(resolve => {
+                    fetchPost("/api/setting/getCloudUser", {}, userResponse => {
+                        window.siyuan.user = userResponse.data;
+                        resolve();
+                    });
+                }));
+            }
+
+            await Promise.all(promises);
+
+            if (!window.siyuan.config.readonly) {
+                bootSync();
+            }
+
+            window.siyuan.menus = new Menus(this);
+            document.title = window.siyuan.languages.siyuanNote;
+            loadAssets(confResponse.data.conf.appearance);
+            initMessage();
+            initAssets();
+            setNoteBook(() => {
+                initFramework(this, confResponse.data.start);
+                initRightMenu(this);
+                openChangelog();
+            });
+            addGA();
         });
     }
 }

+ 2 - 0
app/src/protyle/gutter/index.ts

@@ -1667,6 +1667,7 @@ export class Gutter {
             window.siyuan.menus.menu.append(new MenuItem({
                 label: window.siyuan.languages.wechatReminder,
                 icon: "iconMp",
+                disabled: window.siyuan.config.readonly,
                 click() {
                     openWechatNotify(nodeElement);
                 }
@@ -1678,6 +1679,7 @@ export class Gutter {
                 accelerator: window.siyuan.config.keymap.editor.general.quickMakeCard.custom,
                 iconHTML: '<svg class="b3-menu__icon" style="color:var(--b3-theme-primary)"><use xlink:href="#iconRiffCard"></use></svg>',
                 icon: "iconRiffCard",
+                disabled: window.siyuan.config.readonly,
                 click() {
                     quickMakeCard(protyle, [nodeElement]);
                 }

+ 6 - 0
app/src/protyle/header/openTitleMenu.ts

@@ -112,6 +112,7 @@ export const openTitleMenu = (protyle: IProtyle, position: IPosition) => {
         window.siyuan.menus.menu.append(new MenuItem({
             label: window.siyuan.languages.wechatReminder,
             icon: "iconMp",
+            disabled: window.siyuan.config.readonly,
             click() {
                 openFileWechatNotify(protyle);
             }
@@ -120,6 +121,7 @@ export const openTitleMenu = (protyle: IProtyle, position: IPosition) => {
             iconHTML: "",
             label: window.siyuan.languages.spaceRepetition,
             accelerator: window.siyuan.config.keymap.editor.general.spaceRepetition.custom,
+            disabled: window.siyuan.config.readonly,
             click: () => {
                 fetchPost("/api/riff/getTreeRiffDueCards", {rootID: protyle.block.rootID}, (response) => {
                     openCardByData(protyle.app, response.data, "doc", protyle.block.rootID, response.data.name);
@@ -128,6 +130,7 @@ export const openTitleMenu = (protyle: IProtyle, position: IPosition) => {
         }, {
             iconHTML: "",
             label: window.siyuan.languages.manage,
+            disabled: window.siyuan.config.readonly,
             click: () => {
                 fetchPost("/api/filetree/getHPathByID", {
                     id: protyle.block.rootID
@@ -139,6 +142,7 @@ export const openTitleMenu = (protyle: IProtyle, position: IPosition) => {
             iconHTML: "",
             label: window.siyuan.languages.quickMakeCard,
             accelerator: window.siyuan.config.keymap.editor.general.quickMakeCard.custom,
+            disabled: window.siyuan.config.readonly,
             click: () => {
                 let titleElement = protyle.title?.element;
                 if (!titleElement) {
@@ -153,6 +157,7 @@ export const openTitleMenu = (protyle: IProtyle, position: IPosition) => {
             riffCardMenu.push({
                 iconHTML: "",
                 label: window.siyuan.languages.addToDeck,
+                disabled: window.siyuan.config.readonly,
                 click: () => {
                     makeCard(protyle.app, [protyle.block.rootID]);
                 }
@@ -163,6 +168,7 @@ export const openTitleMenu = (protyle: IProtyle, position: IPosition) => {
             type: "submenu",
             icon: "iconRiffCard",
             submenu: riffCardMenu,
+            disabled: window.siyuan.config.readonly,
         }).element);
 
         window.siyuan.menus.menu.append(new MenuItem({

+ 5 - 5
app/src/protyle/scroll/saveScroll.ts

@@ -45,7 +45,7 @@ export const saveScroll = (protyle: IProtyle, getObject = false) => {
 
 export const getDocByScroll = (options: {
     protyle: IProtyle,
-    scrollAttr: IScrollAttr,
+    scrollAttr?: IScrollAttr,
     mergedOptions?: IOptions,
     cb?: () => void
     focus?: boolean,
@@ -61,7 +61,7 @@ export const getDocByScroll = (options: {
             actions = [Constants.CB_GET_UNUNDO];
         }
     }
-    if (options.scrollAttr.zoomInId) {
+    if (options.scrollAttr?.zoomInId) {
         fetchPost("/api/filetree/getDoc", {
             id: options.scrollAttr.zoomInId,
             size: Constants.SIZE_GET_MAX,
@@ -100,9 +100,9 @@ export const getDocByScroll = (options: {
         return;
     }
     fetchPost("/api/filetree/getDoc", {
-        id: options.scrollAttr.rootId || options.mergedOptions?.blockId || options.protyle.block?.rootID || options.scrollAttr.startId,
-        startID: options.scrollAttr.startId,
-        endID: options.scrollAttr.endId,
+        id: options.scrollAttr?.rootId || options.mergedOptions?.blockId || options.protyle.block?.rootID || options.scrollAttr?.startId,
+        startID: options.scrollAttr?.startId,
+        endID: options.scrollAttr?.endId,
         query: options.protyle.query?.key,
         queryMethod: options.protyle.query?.method,
         queryTypes: options.protyle.query?.types,

+ 4 - 2
app/src/util/assets.ts

@@ -150,8 +150,10 @@ export const initAssets = () => {
                     return;
                 }
             }
-            window.siyuan.config.appearance = response.data.appearance;
-            loadAssets(response.data.appearance);
+            if (response.code === 0) {
+                window.siyuan.config.appearance = response.data.appearance;
+                loadAssets(response.data.appearance);
+            }
         });
     });
 };

+ 27 - 11
app/src/window/index.ts

@@ -27,9 +27,10 @@ class App {
     public appId: string;
 
     constructor() {
+        addBaseURL();
         addScriptSync(`${Constants.PROTYLE_CDN}/js/lute/lute.min.js?v=${Constants.SIYUAN_VERSION}`, "protyleLuteScript");
         addScript(`${Constants.PROTYLE_CDN}/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}`, "protyleWcHtmlScript");
-        addBaseURL();
+
         this.appId = Constants.SIYUAN_APPID;
         window.siyuan = {
             zIndex: 10,
@@ -146,19 +147,34 @@ class App {
         };
         fetchPost("/api/system/getConf", {}, async (response) => {
             window.siyuan.config = response.data.conf;
-            await loadPlugins(this);
-            getLocalStorage(() => {
-                fetchGet(`/appearance/langs/${window.siyuan.config.appearance.lang}.json?v=${Constants.SIYUAN_VERSION}`, (lauguages: IObject) => {
-                    window.siyuan.languages = lauguages;
-                    window.siyuan.menus = new Menus(this);
+
+            const promises = [
+                loadPlugins(this),
+                new Promise<void>(resolve => getLocalStorage(resolve)),
+                new Promise<void>(resolve => fetchGet(
+                    `/appearance/langs/${window.siyuan.config.appearance.lang}.json?v=${Constants.SIYUAN_VERSION}`,
+                    (lauguages: IObject) => {
+                        window.siyuan.languages = lauguages;
+                        resolve();
+                    },
+                )),
+            ];
+
+            if (!window.siyuan.config.readonly) {
+                promises.push(new Promise<void>(resolve => {
                     fetchPost("/api/setting/getCloudUser", {}, userResponse => {
                         window.siyuan.user = userResponse.data;
-                        init(this);
-                        setTitle(window.siyuan.languages.siyuanNote);
-                        initMessage();
+                        resolve();
                     });
-                });
-            });
+                }));
+            }
+
+            await Promise.all(promises);
+
+            window.siyuan.menus = new Menus(this);
+            init(this);
+            setTitle(window.siyuan.languages.siyuanNote);
+            initMessage();
         });
         setNoteBook();
         initBlockPopover(this);

+ 3 - 3
kernel/api/router.go

@@ -47,7 +47,7 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/system/setDownloadInstallPkg", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setDownloadInstallPkg)
 	ginServer.Handle("POST", "/api/system/setNetworkProxy", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setNetworkProxy)
 	ginServer.Handle("POST", "/api/system/setWorkspaceDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setWorkspaceDir)
-	ginServer.Handle("POST", "/api/system/getWorkspaces", model.CheckAuth, model.CheckAdminRole, getWorkspaces)
+	ginServer.Handle("POST", "/api/system/getWorkspaces", model.CheckAuth, getWorkspaces)
 	ginServer.Handle("POST", "/api/system/getMobileWorkspaces", model.CheckAuth, model.CheckAdminRole, getMobileWorkspaces)
 	ginServer.Handle("POST", "/api/system/checkWorkspaceDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, checkWorkspaceDir)
 	ginServer.Handle("POST", "/api/system/createWorkspaceDir", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, createWorkspaceDir)
@@ -238,7 +238,7 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/sync/listCloudSyncDir", model.CheckAuth, model.CheckAdminRole, listCloudSyncDir)
 	ginServer.Handle("POST", "/api/sync/performSync", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, performSync)
 	ginServer.Handle("POST", "/api/sync/performBootSync", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, performBootSync)
-	ginServer.Handle("POST", "/api/sync/getBootSync", model.CheckAuth, getBootSync)
+	ginServer.Handle("POST", "/api/sync/getBootSync", model.CheckAuth, model.CheckAdminRole, getBootSync)
 	ginServer.Handle("POST", "/api/sync/getSyncInfo", model.CheckAuth, model.CheckAdminRole, getSyncInfo)
 	ginServer.Handle("POST", "/api/sync/exportSyncProviderS3", model.CheckAuth, model.CheckAdminRole, exportSyncProviderS3)
 	ginServer.Handle("POST", "/api/sync/importSyncProviderS3", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, importSyncProviderS3)
@@ -318,7 +318,7 @@ func ServeAPI(ginServer *gin.Engine) {
 	ginServer.Handle("POST", "/api/setting/setSearch", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setSearch)
 	ginServer.Handle("POST", "/api/setting/setKeymap", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setKeymap)
 	ginServer.Handle("POST", "/api/setting/setAppearance", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setAppearance)
-	ginServer.Handle("POST", "/api/setting/getCloudUser", model.CheckAuth, getCloudUser)
+	ginServer.Handle("POST", "/api/setting/getCloudUser", model.CheckAuth, model.CheckAdminRole, getCloudUser)
 	ginServer.Handle("POST", "/api/setting/logoutCloudUser", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, logoutCloudUser)
 	ginServer.Handle("POST", "/api/setting/login2faCloudUser", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, login2faCloudUser)
 	ginServer.Handle("POST", "/api/setting/setEmoji", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, setEmoji)

+ 0 - 4
kernel/api/setting.go

@@ -590,10 +590,6 @@ func getCloudUser(c *gin.Context) {
 	ret := gulu.Ret.NewResult()
 	defer c.JSON(http.StatusOK, ret)
 
-	if !model.IsAdminRoleContext(c) {
-		return
-	}
-
 	arg, ok := util.JsonArg(c, ret)
 	if !ok {
 		return

+ 2 - 5
kernel/api/sync.go

@@ -18,13 +18,14 @@ package api
 
 import (
 	"encoding/hex"
-	"github.com/siyuan-note/logging"
 	"io"
 	"net/http"
 	"os"
 	"path/filepath"
 	"time"
 
+	"github.com/siyuan-note/logging"
+
 	"github.com/88250/gulu"
 	"github.com/gin-gonic/gin"
 	"github.com/siyuan-note/siyuan/kernel/conf"
@@ -381,10 +382,6 @@ func getBootSync(c *gin.Context) {
 	ret := gulu.Ret.NewResult()
 	defer c.JSON(http.StatusOK, ret)
 
-	if !model.IsAdminRoleContext(c) {
-		return
-	}
-
 	if model.Conf.Sync.Enabled && 1 == model.BootSyncSucc {
 		ret.Code = 1
 		ret.Msg = model.Conf.Language(17)

+ 7 - 0
kernel/api/workspace.go

@@ -235,6 +235,13 @@ func getWorkspaces(c *gin.Context) {
 		return
 	}
 
+	if role := model.GetGinContextRole(c); !model.IsValidRole(role, []model.Role{
+		model.RoleAdministrator,
+	}) {
+		ret.Data = []*Workspace{}
+		return
+	}
+
 	var workspaces, openedWorkspaces, closedWorkspaces []*Workspace
 	for _, p := range workspacePaths {
 		closed := !util.IsWorkspaceLocked(p)

+ 0 - 4
kernel/model/role.go

@@ -54,7 +54,3 @@ func GetGinContextRole(c *gin.Context) Role {
 		return RoleVisitor
 	}
 }
-
-func IsAdminRoleContext(c *gin.Context) bool {
-	return GetGinContextRole(c) == RoleAdministrator
-}