Browse Source

feat:网站目录支持三级 增加网站目录权限校验 (#2190)

Refs https://github.com/1Panel-dev/1Panel/issues/2140
pull/2196/head^2
zhengkunwang 1 year ago committed by GitHub
parent
commit
4d8118d9ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      backend/app/api/v1/website.go
  2. 4
      backend/app/dto/request/website.go
  3. 7
      backend/app/dto/response/website.go
  4. 58
      backend/app/service/website.go
  5. 4
      backend/app/service/website_utils.go
  6. 1
      backend/i18n/lang/en.yaml
  7. 1
      backend/i18n/lang/zh-Hant.yaml
  8. 1
      backend/i18n/lang/zh.yaml
  9. 1
      backend/router/ro_website.go
  10. 101
      cmd/server/docs/docs.go
  11. 97
      cmd/server/docs/swagger.json
  12. 59
      cmd/server/docs/swagger.yaml
  13. 7
      frontend/src/api/interface/website.ts
  14. 4
      frontend/src/api/modules/website.ts
  15. 57
      frontend/src/views/website/website/config/basic/site-folder/index.vue

22
backend/app/api/v1/website.go

@ -912,3 +912,25 @@ func (b *BaseApi) UpdateRedirectConfigFile(c *gin.Context) {
}
helper.SuccessWithOutData(c)
}
// @Tags Website
// @Summary Get website dir
// @Description 获取网站目录配置
// @Accept json
// @Param request body request.WebsiteCommonReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/dir [post]
func (b *BaseApi) GetDirConfig(c *gin.Context) {
var req request.WebsiteCommonReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
res, err := websiteService.LoadWebsiteDirConfig(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}

4
backend/app/dto/request/website.go

@ -213,3 +213,7 @@ type WebsiteWafFileUpdate struct {
Content string `json:"content" validate:"required"`
Type string `json:"type" validate:"required,oneof=cc ip_white ip_block url_white url_block cookie_block args_check post_check ua_check file_ext_block"`
}
type WebsiteCommonReq struct {
ID uint `json:"id" validate:"required"`
}

7
backend/app/dto/response/website.go

@ -53,3 +53,10 @@ type PHPConfig struct {
type NginxRewriteRes struct {
Content string `json:"content"`
}
type WebsiteDirConfig struct {
Dirs []string `json:"dirs"`
User string `json:"user"`
UserGroup string `json:"userGroup"`
Msg string `json:"msg"`
}

58
backend/app/service/website.go

@ -9,13 +9,16 @@ import (
"encoding/pem"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/spf13/afero"
"os"
"path"
"reflect"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
@ -80,6 +83,7 @@ type IWebsiteService interface {
GetRewriteConfig(req request.NginxRewriteReq) (*response.NginxRewriteRes, error)
UpdateRewriteConfig(req request.NginxRewriteUpdate) error
LoadWebsiteDirConfig(req request.WebsiteCommonReq) (*response.WebsiteDirConfig, error)
UpdateSiteDir(req request.WebsiteUpdateDir) error
UpdateSitePermission(req request.WebsiteUpdateDirPermission) error
OperateProxy(req request.WebsiteProxyConfig) (err error)
@ -1389,7 +1393,7 @@ func (w WebsiteService) UpdateSiteDir(req request.WebsiteUpdateDir) error {
runDir := req.SiteDir
siteDir := path.Join("/www/sites", website.Alias, "index")
if req.SiteDir != "/" {
siteDir = fmt.Sprintf("%s/%s", siteDir, req.SiteDir)
siteDir = fmt.Sprintf("%s%s", siteDir, req.SiteDir)
}
if err := updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "root", Params: []string{siteDir}}}, &website); err != nil {
return err
@ -1408,9 +1412,6 @@ func (w WebsiteService) UpdateSitePermission(req request.WebsiteUpdateDirPermiss
return err
}
absoluteIndexPath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index")
if website.SiteDir != "/" {
absoluteIndexPath = path.Join(absoluteIndexPath, website.SiteDir)
}
chownCmd := fmt.Sprintf("chown -R %s:%s %s", req.User, req.Group, absoluteIndexPath)
if cmd.HasNoPasswordSudo() {
chownCmd = fmt.Sprintf("sudo %s", chownCmd)
@ -2318,3 +2319,52 @@ func (w WebsiteService) UpdateWafFile(req request.WebsiteWafFileUpdate) (err err
rulePath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "waf", "rules", fmt.Sprintf("%s.json", req.Type))
return files.NewFileOp().WriteFile(rulePath, strings.NewReader(req.Content), 0755)
}
func (w WebsiteService) LoadWebsiteDirConfig(req request.WebsiteCommonReq) (*response.WebsiteDirConfig, error) {
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return nil, err
}
res := &response.WebsiteDirConfig{}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return nil, err
}
absoluteIndexPath := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index")
var appFs = afero.NewOsFs()
info, err := appFs.Stat(absoluteIndexPath)
if err != nil {
return nil, err
}
res.User = strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Uid), 10)
res.UserGroup = strconv.FormatUint(uint64(info.Sys().(*syscall.Stat_t).Gid), 10)
indexFiles, err := os.ReadDir(absoluteIndexPath)
if err != nil {
return nil, err
}
res.Dirs = []string{"/"}
for _, file := range indexFiles {
if !file.IsDir() {
continue
}
res.Dirs = append(res.Dirs, fmt.Sprintf("/%s", file.Name()))
fileInfo, _ := file.Info()
if fileInfo.Sys().(*syscall.Stat_t).Uid != 1000 || fileInfo.Sys().(*syscall.Stat_t).Gid != 1000 {
res.Msg = i18n.GetMsgByKey("ErrPathPermission")
}
childFiles, _ := os.ReadDir(absoluteIndexPath + "/" + file.Name())
for _, childFile := range childFiles {
if !childFile.IsDir() {
continue
}
childInfo, _ := childFile.Info()
if childInfo.Sys().(*syscall.Stat_t).Uid != 1000 || childInfo.Sys().(*syscall.Stat_t).Gid != 1000 {
res.Msg = i18n.GetMsgByKey("ErrPathPermission")
}
res.Dirs = append(res.Dirs, fmt.Sprintf("/%s/%s", file.Name(), childFile.Name()))
}
}
return res, nil
}

4
backend/app/service/website_utils.go

@ -639,3 +639,7 @@ func chownRootDir(path string) error {
}
return nil
}
func checkWebsiteDirPermission(path string) error {
return nil
}

1
backend/i18n/lang/en.yaml

@ -66,6 +66,7 @@ ErrGroupIsUsed: 'The group is in use and cannot be deleted'
ErrBackupMatch: 'the backup file does not match the current partial data of the website: {{ .detail}}"'
ErrBackupExist: 'the backup file corresponds to a portion of the original data that does not exist: {{ .detail}}"'
ErrPHPResource: 'The local runtime does not support switching!'
ErrPathPermission: 'A folder with non-1000:1000 permissions was detected in the index directory, which may cause Access denied errors when accessing the website.'
#ssl
ErrSSLCannotDelete: "The certificate is being used by the website and cannot be removed"

1
backend/i18n/lang/zh-Hant.yaml

@ -66,6 +66,7 @@ ErrGroupIsUsed: '分組正在使用中,無法刪除'
ErrBackupMatch: '該備份文件與當前網站部分數據不匹配: {{ .detail}}"'
ErrBackupExist: '該備份文件對應部分原數據不存在: {{ .detail}}"'
ErrPHPResource: '本地運行環境不支持切換!'
ErrPathPermission: 'index 目錄下檢測到非 1000:1000 權限文件夾,可能導致網站訪問 Access denied 錯誤'
#ssl
ErrSSLCannotDelete: "證書正在被網站使用,無法刪除"

1
backend/i18n/lang/zh.yaml

@ -66,6 +66,7 @@ ErrGroupIsUsed: '分组正在使用中,无法删除'
ErrBackupMatch: '该备份文件与当前网站部分数据不匹配 {{ .detail}}"'
ErrBackupExist: '该备份文件对应部分源数据不存在 {{ .detail}}"'
ErrPHPResource: '本地运行环境不支持切换!'
ErrPathPermission: 'index 目录下检测到非 1000:1000 权限文件夹,可能导致网站访问 Access denied 错误'
#ssl
ErrSSLCannotDelete: "证书正在被网站使用,无法删除"

1
backend/router/ro_website.go

@ -53,6 +53,7 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) {
groupRouter.POST("/dir/update", baseApi.UpdateSiteDir)
groupRouter.POST("/dir/permission", baseApi.UpdateSiteDirPermission)
groupRouter.POST("/dir", baseApi.GetDirConfig)
groupRouter.POST("/proxies", baseApi.GetProxyConfig)
groupRouter.POST("/proxies/update", baseApi.UpdateProxyConfig)

101
cmd/server/docs/docs.go

@ -1,5 +1,5 @@
// Code generated by swaggo/swag. DO NOT EDIT.
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import "github.com/swaggo/swag"
@ -6446,31 +6446,6 @@ const docTemplate = `{
}
}
},
"/host/tool/supervisor/process/load": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Supervisor 进程状态",
"tags": [
"Host tool"
],
"summary": "Load Supervisor process status",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/response.ProcessStatus"
}
}
}
}
}
},
"/hosts": {
"post": {
"security": [
@ -9907,6 +9882,39 @@ const docTemplate = `{
}
}
},
"/websites/dir": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取网站目录配置",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Get website dir",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCommonReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/websites/dir/permission": {
"post": {
"security": [
@ -12781,10 +12789,7 @@ const docTemplate = `{
"type": "integer"
},
"type": {
"type": "string",
"enum": [
"mysql"
]
"type": "string"
},
"username": {
"type": "string"
@ -12822,6 +12827,9 @@ const docTemplate = `{
"port": {
"type": "integer"
},
"type": {
"type": "string"
},
"username": {
"type": "string"
},
@ -16401,6 +16409,17 @@ const docTemplate = `{
}
}
},
"request.WebsiteCommonReq": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer"
}
}
},
"request.WebsiteCreate": {
"type": "object",
"required": [
@ -17444,26 +17463,6 @@ const docTemplate = `{
}
}
},
"response.ProcessStatus": {
"type": "object",
"properties": {
"PID": {
"type": "string"
},
"msg": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string"
},
"uptime": {
"type": "string"
}
}
},
"response.WebsiteAcmeAccountDTO": {
"type": "object",
"properties": {

97
cmd/server/docs/swagger.json

@ -6439,31 +6439,6 @@
}
}
},
"/host/tool/supervisor/process/load": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Supervisor 进程状态",
"tags": [
"Host tool"
],
"summary": "Load Supervisor process status",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/response.ProcessStatus"
}
}
}
}
}
},
"/hosts": {
"post": {
"security": [
@ -9900,6 +9875,39 @@
}
}
},
"/websites/dir": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取网站目录配置",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Get website dir",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteCommonReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/websites/dir/permission": {
"post": {
"security": [
@ -12774,10 +12782,7 @@
"type": "integer"
},
"type": {
"type": "string",
"enum": [
"mysql"
]
"type": "string"
},
"username": {
"type": "string"
@ -12815,6 +12820,9 @@
"port": {
"type": "integer"
},
"type": {
"type": "string"
},
"username": {
"type": "string"
},
@ -16394,6 +16402,17 @@
}
}
},
"request.WebsiteCommonReq": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer"
}
}
},
"request.WebsiteCreate": {
"type": "object",
"required": [
@ -17437,26 +17456,6 @@
}
}
},
"response.ProcessStatus": {
"type": "object",
"properties": {
"PID": {
"type": "string"
},
"msg": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string"
},
"uptime": {
"type": "string"
}
}
},
"response.WebsiteAcmeAccountDTO": {
"type": "object",
"properties": {

59
cmd/server/docs/swagger.yaml

@ -737,8 +737,6 @@ definitions:
port:
type: integer
type:
enum:
- mysql
type: string
username:
type: string
@ -771,6 +769,8 @@ definitions:
type: string
port:
type: integer
type:
type: string
username:
type: string
version:
@ -3169,6 +3169,13 @@ definitions:
required:
- email
type: object
request.WebsiteCommonReq:
properties:
id:
type: integer
required:
- id
type: object
request.WebsiteCreate:
properties:
IPV6:
@ -3869,19 +3876,6 @@ definitions:
uploadMaxSize:
type: string
type: object
response.ProcessStatus:
properties:
PID:
type: string
msg:
type: string
name:
type: string
status:
type: string
uptime:
type: string
type: object
response.WebsiteAcmeAccountDTO:
properties:
createdAt:
@ -8116,21 +8110,6 @@ paths:
formatEN: '[operate] Supervisor Process Config file'
formatZH: '[operate] Supervisor 进程文件 '
paramKeys: []
/host/tool/supervisor/process/load:
post:
description: 获取 Supervisor 进程状态
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/response.ProcessStatus'
type: array
security:
- ApiKeyAuth: []
summary: Load Supervisor process status
tags:
- Host tool
/hosts:
post:
consumes:
@ -10305,6 +10284,26 @@ paths:
formatEN: Delete website [domain]
formatZH: 删除网站 [domain]
paramKeys: []
/websites/dir:
post:
consumes:
- application/json
description: 获取网站目录配置
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteCommonReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Get website dir
tags:
- Website
/websites/dir/permission:
post:
consumes:

7
frontend/src/api/interface/website.ts

@ -431,4 +431,11 @@ export namespace Website {
runtimeID: number;
retainConfig: boolean;
}
export interface DirConfig {
dirs: string[];
user: string;
userGroup: string;
msg: string;
}
}

4
frontend/src/api/modules/website.ts

@ -234,3 +234,7 @@ export const UpdateRedirectConfigFile = (req: Website.RedirectFileUpdate) => {
export const ChangePHPVersion = (req: Website.PHPVersionChange) => {
return http.post<any>(`/websites/php/version`, req);
};
export const GetDirConfig = (req: Website.ProxyReq) => {
return http.post<Website.DirConfig>(`/websites/dir`, req);
};

57
frontend/src/views/website/website/config/basic/site-folder/index.vue

@ -21,7 +21,6 @@
<el-form-item v-if="configDir" :label="$t('website.runDir')">
<el-space wrap>
<el-select v-model="update.siteDir">
<el-option :label="'/'" :value="'/'"></el-option>
<el-option
v-for="(item, index) in dirs"
:label="item"
@ -56,6 +55,11 @@
<span class="warnHelper">{{ $t('website.runUserHelper') }}</span>
</template>
</el-alert>
<el-alert :closable="false" type="error" v-if="dirConfig.msg != ''">
<template #default>
<span class="warnHelper">{{ dirConfig.msg }}</span>
</template>
</el-alert>
<br />
<el-descriptions :title="$t('website.folderTitle')" :column="1" border>
<el-descriptions-item label="waf">{{ $t('website.wafFolder') }}</el-descriptions-item>
@ -67,8 +71,8 @@
</div>
</template>
<script lang="ts" setup>
import { GetFilesList } from '@/api/modules/files';
import { GetWebsite, UpdateWebsiteDir, UpdateWebsiteDirPermission } from '@/api/modules/website';
import { Website } from '@/api/interface/website';
import { GetDirConfig, GetWebsite, UpdateWebsiteDir, UpdateWebsiteDirPermission } from '@/api/modules/website';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
@ -98,17 +102,13 @@ const updatePermission = reactive({
group: '1000',
});
const siteForm = ref<FormInstance>();
const dirReq = reactive({
path: '/',
expand: true,
showHidden: false,
page: 1,
pageSize: 100,
search: '',
containSub: false,
dir: true,
});
const dirs = ref([]);
const dirConfig = ref<Website.DirConfig>({
dirs: [''],
user: '',
userGroup: '',
msg: '',
});
const search = () => {
loading.value = true;
@ -116,14 +116,15 @@ const search = () => {
.then((res) => {
website.value = res.data;
update.id = website.value.id;
update.siteDir = website.value.siteDir;
update.siteDir = website.value.siteDir.startsWith('/')
? website.value.siteDir
: '/' + website.value.siteDir;
updatePermission.id = website.value.id;
updatePermission.group = website.value.group === '' ? '1000' : website.value.group;
updatePermission.user = website.value.user === '' ? '1000' : website.value.user;
if (website.value.type === 'static' || website.value.runtimeID > 0) {
configDir.value = true;
dirReq.path = website.value.sitePath + '/index';
getDirs();
getDirConfig();
}
})
.finally(() => {
@ -164,25 +165,19 @@ const submitPermission = async () => {
});
};
const getDirs = async () => {
loading.value = true;
await GetFilesList(dirReq)
.then((res) => {
dirs.value = [];
const items = res.data.items || [];
for (const item of items) {
dirs.value.push(item.name);
}
})
.finally(() => {
loading.value = false;
});
};
const initData = () => {
dirs.value = [];
};
const getDirConfig = async () => {
try {
const res = await GetDirConfig({ id: props.id });
dirs.value = res.data.dirs;
dirConfig.value = res.data;
console.log(res);
} catch (error) {}
};
const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } });
};

Loading…
Cancel
Save