新建/编辑任务添加安全选项检查,添加消息组件

pull/1/head
Doflatango 2017-03-09 11:39:06 +08:00 committed by miraclesu
parent a2c31890ef
commit fe3dfd4429
18 changed files with 240 additions and 94 deletions

4
.gitignore vendored
View File

@ -3,4 +3,6 @@ conf/files/*.json
.tags_sorted_by_file
bin/*/*server
.DS_Store
web/ui/node_modules
web/ui/node_modules
.vscode
*npm-debug.log

View File

@ -14,5 +14,6 @@ var (
ErrEmptyNodeGroupName = errors.New("Name of node group is empty.")
ErrIllegalNodeGroupId = errors.New("Invalid node group id that includes illegal characters such as '/'.")
InvalidJobErr = errors.New("invalid job")
ErrSecurityInvalidCmd = errors.New("Security error: the suffix of script file is not on the whitelist.")
ErrSecurityInvalidUser = errors.New("Security error: the user is not on the whitelist.")
)

View File

@ -121,8 +121,8 @@ func GetJobs() (jobs map[string]*Job, err error) {
continue
}
if !job.Valid() {
log.Warnf("job[%s] is invalid", string(j.Key))
if err := job.Valid(); err != nil {
log.Warnf("job[%s] is invalid: %s", string(j.Key), err.Error())
continue
}
@ -142,9 +142,7 @@ func GetJobFromKv(kv *mvccpb.KeyValue) (job *Job, err error) {
return
}
if !job.Valid() {
err = InvalidJobErr
}
err = job.Valid()
return
}
@ -271,7 +269,7 @@ func (j *Job) Check() error {
return ErrEmptyJobCommand
}
return nil
return j.Valid()
}
// 执行结果写入 mongoDB
@ -309,17 +307,25 @@ func (j *Job) Cmds(nid string, gs map[string]*Group) (cmds map[string]*Cmd) {
}
// 安全选项验证
func (j *Job) Valid() bool {
func (j *Job) Valid() error {
if len(j.cmd) == 0 {
j.splitCmd()
}
security := conf.Config.Security
if !security.Open {
return true
return nil
}
return j.validUser() && j.validCmd()
if !j.validUser() {
return ErrSecurityInvalidUser
}
if !j.validCmd() {
return ErrSecurityInvalidCmd
}
return nil
}
func (j *Job) validUser() bool {
@ -339,7 +345,6 @@ func (j *Job) validCmd() bool {
if len(conf.Config.Security.Ext) == 0 {
return true
}
for _, ext := range conf.Config.Security.Ext {
if strings.HasSuffix(j.cmd[0], ext) {
return true

View File

@ -27,7 +27,7 @@ func (b BaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
buf.Write(debug.Stack())
stack = buf.String()
outJSONError(w, http.StatusInternalServerError, "Internal Server Error")
outJSONWithCode(w, http.StatusInternalServerError, "Internal Server Error")
log.Errorf("%v\n\n%s\n", err_, stack)
return
@ -53,10 +53,3 @@ func outJSONWithCode(w http.ResponseWriter, httpCode int, data interface{}) {
func outJSON(w http.ResponseWriter, data interface{}) {
outJSONWithCode(w, http.StatusOK, data)
}
func outJSONError(w http.ResponseWriter, httpCode int, msg string) {
r := map[string]string{
"error": msg,
}
outJSONWithCode(w, httpCode, r)
}

36
web/configuration.go Normal file
View File

@ -0,0 +1,36 @@
package web
import (
"net/http"
"sunteng/cronsun/conf"
)
type Configuration struct {
Security *securityCnf `json:"security"`
}
type securityCnf struct {
Enable bool `json:"enable"`
AllowUsers []string `json:"allowUsers,omitempty"`
AllowSuffixs []string `json:"allowSuffixs,omitempty"`
}
func NewConfiguration() *Configuration {
cnf := &Configuration{
Security: &securityCnf{
Enable: conf.Config.Security.Open,
},
}
if conf.Config.Security.Open {
cnf.Security.AllowUsers = conf.Config.Security.Users
cnf.Security.AllowSuffixs = conf.Config.Security.Ext
}
return cnf
}
func (cnf *Configuration) Configuratios(w http.ResponseWriter, r *http.Request) {
outJSON(w, cnf)
}

View File

@ -26,7 +26,7 @@ func (j *Job) GetJob(w http.ResponseWriter, r *http.Request) {
} else {
statusCode = http.StatusInternalServerError
}
outJSONError(w, statusCode, err.Error())
outJSONWithCode(w, statusCode, err.Error())
return
}
@ -37,7 +37,7 @@ func (j *Job) DeleteJob(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_, err := models.DeleteJob(vars["group"], vars["id"])
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
@ -49,7 +49,7 @@ func (j *Job) ChangeJobStatus(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&job)
if err != nil {
outJSONError(w, http.StatusBadRequest, err.Error())
outJSONWithCode(w, http.StatusBadRequest, err.Error())
return
}
r.Body.Close()
@ -57,20 +57,20 @@ func (j *Job) ChangeJobStatus(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
originJob, rev, err := models.GetJobAndRev(vars["group"], vars["id"])
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
originJob.Pause = job.Pause
b, err := json.Marshal(originJob)
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
_, err = models.DefalutClient.PutWithModRev(originJob.Key(), string(b), rev)
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
@ -86,13 +86,13 @@ func (j *Job) UpdateJob(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&job)
if err != nil {
outJSONError(w, http.StatusBadRequest, err.Error())
outJSONWithCode(w, http.StatusBadRequest, err.Error())
return
}
r.Body.Close()
if err = job.Check(); err != nil {
outJSONError(w, http.StatusBadRequest, err.Error())
outJSONWithCode(w, http.StatusBadRequest, err.Error())
return
}
@ -110,13 +110,13 @@ func (j *Job) UpdateJob(w http.ResponseWriter, r *http.Request) {
b, err := json.Marshal(job)
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
_, err = models.DefalutClient.Put(job.Key(), string(b))
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
@ -133,7 +133,7 @@ func (j *Job) UpdateJob(w http.ResponseWriter, r *http.Request) {
func (j *Job) GetGroups(w http.ResponseWriter, r *http.Request) {
resp, err := models.DefalutClient.Get(conf.Config.Cmd, clientv3.WithPrefix(), clientv3.WithKeysOnly())
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
@ -168,7 +168,7 @@ func (j *Job) GetList(w http.ResponseWriter, r *http.Request) {
resp, err := models.DefalutClient.Get(prefix, clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
@ -178,7 +178,7 @@ func (j *Job) GetList(w http.ResponseWriter, r *http.Request) {
job := models.Job{}
err = json.Unmarshal(resp.Kvs[i].Value, &job)
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
jobList = append(jobList, &jobStatus{Job: &job})

View File

@ -105,7 +105,7 @@ func (jl *JobLog) GetList(w http.ResponseWriter, r *http.Request) {
pager.List, pager.Total, err = models.GetJobLogList(query, page, pageSize, sort)
}
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}

View File

@ -23,7 +23,7 @@ func (n *Node) UpdateGroup(w http.ResponseWriter, r *http.Request) {
de := json.NewDecoder(r.Body)
var err error
if err = de.Decode(&g); err != nil {
outJSONError(w, http.StatusBadRequest, err.Error())
outJSONWithCode(w, http.StatusBadRequest, err.Error())
return
}
defer r.Body.Close()
@ -36,14 +36,14 @@ func (n *Node) UpdateGroup(w http.ResponseWriter, r *http.Request) {
}
if err = g.Check(); err != nil {
outJSONError(w, http.StatusBadRequest, err.Error())
outJSONWithCode(w, http.StatusBadRequest, err.Error())
return
}
// @TODO modRev
var modRev int64 = 0
if _, err = g.Put(modRev); err != nil {
outJSONError(w, http.StatusBadRequest, err.Error())
outJSONWithCode(w, http.StatusBadRequest, err.Error())
return
}
@ -53,7 +53,7 @@ func (n *Node) UpdateGroup(w http.ResponseWriter, r *http.Request) {
func (n *Node) GetGroups(w http.ResponseWriter, r *http.Request) {
resp, err := models.DefalutClient.Get(conf.Config.Group, v3.WithPrefix(), v3.WithSort(v3.SortByKey, v3.SortAscend))
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
@ -63,7 +63,7 @@ func (n *Node) GetGroups(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(resp.Kvs[i].Value, &g)
if err != nil {
log.Errorf("node.GetGroups(key: %s) error: %s", string(resp.Kvs[i].Key), err.Error())
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
list = append(list, &g)
@ -76,7 +76,7 @@ func (n *Node) GetGroupByGroupId(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
g, err := models.GetGroupById(vars["id"])
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
@ -91,13 +91,13 @@ func (n *Node) DeleteGroup(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
groupId := strings.TrimSpace(vars["id"])
if len(groupId) == 0 {
outJSONError(w, http.StatusBadRequest, "empty node ground id.")
outJSONWithCode(w, http.StatusBadRequest, "empty node ground id.")
return
}
_, err := models.DeleteGroupById(groupId)
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}
@ -105,7 +105,7 @@ func (n *Node) DeleteGroup(w http.ResponseWriter, r *http.Request) {
if err != nil {
errstr := fmt.Sprintf("failed to fetch jobs from etcd after deleted node group[%s]: %s", groupId, err.Error())
log.Error(errstr)
outJSONError(w, http.StatusInternalServerError, errstr)
outJSONWithCode(w, http.StatusInternalServerError, errstr)
return
}
@ -153,7 +153,7 @@ func (n *Node) DeleteGroup(w http.ResponseWriter, r *http.Request) {
func (n *Node) GetNodes(w http.ResponseWriter, r *http.Request) {
nodes, err := models.GetNodes()
if err != nil {
outJSONError(w, http.StatusInternalServerError, err.Error())
outJSONWithCode(w, http.StatusInternalServerError, err.Error())
return
}

View File

@ -14,6 +14,7 @@ func InitRouters() (s *http.Server, err error) {
nodeHandler := &Node{}
jobLogHandler := &JobLog{}
infoHandler := &Info{}
configHandler := NewConfiguration()
r := mux.NewRouter()
subrouter := r.PathPrefix("/v1").Subrouter()
@ -62,6 +63,9 @@ func InitRouters() (s *http.Server, err error) {
h = BaseHandler{Handle: infoHandler.Overview}
subrouter.Handle("/info/overview", h).Methods("GET")
h = BaseHandler{Handle: configHandler.Configuratios}
subrouter.Handle("/configurations", h).Methods("GET")
uidir := conf.Config.Web.UIDir
if len(uidir) == 0 {
uidir = path.Join("web", "ui", "dist")

32
web/ui/dist/build.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,16 +4,20 @@
<meta charset="utf-8">
<title>Cronsun Managerment</title>
<style>
.initial.error {
margin: 170px 80px;
color: red;
font-size: 2em;
line-height: 2em;
}
.loader, .loader:after {border-radius: 50%; width: 10em; height: 10em;}
.loader {
margin: 180px auto;
font-size: 10px;
position: relative;
text-indent: -9999em;
border-top: 1.1em solid rgba(128,0,128, 0.2);
border-right: 1.1em solid rgba(128,0,128, 0.2);
border-bottom: 1.1em solid rgba(128,0,128, 0.2);
border-left: 1.1em solid #800080;
border: 1.1em solid rgba(9, 47, 181, 0.2);
border-left: 1.1em solid #2185d0;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);

View File

@ -4,16 +4,20 @@
<meta charset="utf-8">
<title>Cronsun Managerment</title>
<style>
.initial.error {
margin: 170px 80px;
color: red;
font-size: 2em;
line-height: 2em;
}
.loader, .loader:after {border-radius: 50%; width: 10em; height: 10em;}
.loader {
margin: 180px auto;
font-size: 10px;
position: relative;
text-indent: -9999em;
border-top: 1.1em solid rgba(128,0,128, 0.2);
border-right: 1.1em solid rgba(128,0,128, 0.2);
border-bottom: 1.1em solid rgba(128,0,128, 0.2);
border-left: 1.1em solid #800080;
border: 1.1em solid rgba(9, 47, 181, 0.2);
border-left: 1.1em solid #2185d0;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);

View File

@ -11,11 +11,17 @@
<div class="ui container">
<router-view></router-view>
</div>
<Messager/>
</div>
</template>
<script>
import Messager from './components/Messager.vue';
export default {
name: 'app'
name: 'app',
components: {
Messager
}
}
</script>

View File

@ -95,11 +95,6 @@ export default {
},
mounted(){
// var formatNumber = function(i, len){
// var n = i == 0 ? 1 : Math.ceil(Math.log10(i+1));
// if (n >= len) return i.toString();
// return '0'.repeat(len-n) + i.toString();
// }
var d = new Date()
this.today = d.getFullYear().toString() + '-' + formatNumber(d.getMonth()+1, 2) + '-' + d.getDate();

View File

@ -17,17 +17,18 @@
</div>
<div class="field">
<label>任务分组</label>
<Dropdown title="选择分组" v-bind:items="groups" v-bind:selected="job.group" v-on:change="changeGroup"/>
<Dropdown title="选择分组" v-bind:items="groups" v-bind:selected="job.group" v-on:change="changeGroup"></Dropdown>
</div>
</div>
<div class="fields">
<div class="twelve wide field">
<label>任务脚本</label>
<label>任务脚本 {{allowSuffixsTip}}</label>
<input type="text" v-model="job.cmd" placeholder="任务脚本">
</div>
<div class="four wide field">
<label>用户(可选)</label>
<input type="text" v-model="job.user" placeholder="指定执行脚本的用户">
<label>用户({{$appConfig.security.enable ? '必选' : '可选'}})</label>
<Dropdown v-if="$appConfig.security.enable" title="指定执行用户" v-bind:items="$appConfig.security.allowUsers" v-bind:selected="job.user" v-on:change="changeUser"></Dropdown>
<input v-else type="text" v-model="job.user" placeholder="指定执行用户">
</div>
</div>
<div class="field">
@ -56,6 +57,7 @@ export default {
action: 'CREATE',
groups: [],
loading: false,
allowSuffixsTip: '',
job: {
id: '',
name: '',
@ -88,6 +90,10 @@ export default {
this.job.group = val;
},
changeUser: function(val, text){
this.job.user = val;
},
removeRule: function(index){
this.job.rules.splice(index, 1);
},
@ -102,7 +108,7 @@ export default {
var vm = this;
this.$rest.PUT('job', this.job)
.onsucceed(exceptCode, ()=>{vm.$router.push('/job')})
.onfailed((resp)=>{console.log(resp)})
.onfailed((resp)=>{vm.$bus.$emit('error', resp)})
.onend(()=>{vm.loading=false})
.do();
},
@ -114,6 +120,12 @@ export default {
mounted: function(){
var vm = this;
var secCnf = this.$appConfig.security;
if (secCnf.enable) {
if (secCnf.allowSuffixs && secCnf.allowSuffixs.length > 0) {
this.allowSuffixsTip = '(当前限制只允许添加此类后缀脚本:' + secCnf.allowSuffixs.join(' ') + '';
}
}
if (this.$route.path.indexOf('/job/create') === 0) {
this.action = 'CREATE';
@ -145,13 +157,6 @@ export default {
vm.job.pause = !vm.job.pause;
}
});
$(this.$el).find('.dropdown').dropdown({
allowAdditions: true,
onChange: function(value, text, $choice){
vm.job.group = value;
}
}).dropdown('set exactly', this.job.group);
},
components: {

View File

@ -0,0 +1,71 @@
<style scope>
.show {}
</style>
<template>
<div class="ui sticky fixed" style="top: 80px; right: 20px; width: 400px;">
<div v-for="(m, index) in queue" :key="m.id" class="ui floating message transition animate fly left" :class="[m.type, m.animation, m.visiable]">
<i class="close icon" v-on:click="closeMessage(m.id)"></i>
<div class="header">{{m.content}}</div>
</div>
</div>
</template>
<script>
export default {
name: 'message',
data(){
return {
queue: []
}
},
methods: {
showMessage(type, content){
var id = Math.random().toString();
this.queue.push({
id: id,
content: content,
type: type,
animation: 'in',
visiable: 'visiable'
});
var vm = this;
setTimeout(()=>{
vm.closeMessage(id);
}, 5000);
},
closeMessage(id){
var vm = this;
for (var i in vm.queue) {
if (vm.queue[i].id === id) {
vm.queue[i].animation = 'out';
setTimeout(()=>{
for (var i in vm.queue) {
if (vm.queue[i].id === id) {
vm.queue.splice(i, 1);
return;
}
}
}, 600);
break;
}
}
}
},
mounted(){
var vm = this;
this.$bus.$on('error', (content)=>{
vm.showMessage('error', content);
});
this.$bus.$on('success', (content)=>{
vm.showMessage('success', content);
});
this.$bus.$on('warning', (content)=>{
vm.showMessage('warning', content);
});
}
}
</script>

View File

@ -7,8 +7,9 @@ Vue.config.debug = true;
// global restful client
import Rest from './libraries/rest-client.js';
const RestApi =(Vue, options)=>{
Vue.prototype.$rest = new Rest('/v1/');
var restApi = new Rest('/v1/');
const RestApi = (Vue, options)=>{
Vue.prototype.$rest = restApi;
};
Vue.use(RestApi);
@ -47,8 +48,27 @@ var router = new VueRouter({
routes: routes
});
var app = new Vue({
el: '#app',
render: h => h(App),
router: router
});
restApi.GET('configurations').onsucceed(200, (resp)=>{
const Config = (Vue, options)=>{
Vue.prototype.$appConfig = resp;
}
Vue.use(Config);
var app = new Vue({
el: '#app',
render: h => h(App),
router: router
});
}).onfailed((data, xhr)=>{
var msg = data ? data : xhr.status+' '+xhr.statusText;
showInitialError('Failed to get global configurations('+xhr.responseURL+'): '+msg);
}).onexception((msg)=>{
showInitialError('Failed to get global configurations('+xhr.responseURL+'): '+msg);
}).do();
function showInitialError(msg) {
var d = document.getElementById('app');
d.innerHTML = msg;
d.className = 'initial error';
}