mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 14:51:57 +00:00
Add permission control for service resource
This commit is contained in:
parent
8ce6adf478
commit
1f652bb6f3
@ -1350,6 +1350,63 @@ var Swirl;
|
||||
})(Node = Swirl.Node || (Swirl.Node = {}));
|
||||
})(Swirl || (Swirl = {}));
|
||||
var Swirl;
|
||||
(function (Swirl) {
|
||||
var Perm;
|
||||
(function (Perm) {
|
||||
var Dispatcher = Swirl.Core.Dispatcher;
|
||||
var Modal = Swirl.Core.Modal;
|
||||
class EditPage {
|
||||
constructor() {
|
||||
$("#txt-query").keydown(this.searchUser);
|
||||
$("#btn-add-user").click(this.addUser);
|
||||
Dispatcher.bind("#div-users").on("delete-user", this.deleteUser.bind(this));
|
||||
}
|
||||
deleteUser(e) {
|
||||
$(e.target).closest("div.control").remove();
|
||||
}
|
||||
searchUser(e) {
|
||||
if (e.keyCode == 13) {
|
||||
let query = $.trim($(e.target).val());
|
||||
if (query.length == 0) {
|
||||
return;
|
||||
}
|
||||
$ajax.post("/system/user/search", { query: query }).encoder("form").json((users) => {
|
||||
let $panel = $("#nav-users");
|
||||
$panel.find("label.panel-block").remove();
|
||||
for (let user of users) {
|
||||
$panel.append(`<label class="panel-block">
|
||||
<input type="checkbox" value="${user.id}" data-name="${user.name}"> ${user.name}
|
||||
</label>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
addUser() {
|
||||
let users = {};
|
||||
$("#div-users").find("input").each((i, e) => {
|
||||
users[$(e).val()] = true;
|
||||
});
|
||||
let $panel = $("#nav-users");
|
||||
$panel.find("input:checked").each((i, e) => {
|
||||
let $el = $(e);
|
||||
if (users[$el.val()]) {
|
||||
return;
|
||||
}
|
||||
$("#div-users").append(`<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-info">${$el.data("name")}</span>
|
||||
<a class="tag is-delete" data-action="delete-user"></a>
|
||||
<input name="users[]" value="${$el.val()}" type="hidden">
|
||||
</div>`);
|
||||
});
|
||||
Modal.close();
|
||||
$panel.find("label.panel-block").remove();
|
||||
}
|
||||
}
|
||||
Perm.EditPage = EditPage;
|
||||
})(Perm = Swirl.Perm || (Swirl.Perm = {}));
|
||||
})(Swirl || (Swirl = {}));
|
||||
var Swirl;
|
||||
(function (Swirl) {
|
||||
var Registry;
|
||||
(function (Registry) {
|
||||
@ -1762,49 +1819,35 @@ var Swirl;
|
||||
this.table.on("delete-service", this.deleteService.bind(this))
|
||||
.on("scale-service", this.scaleService.bind(this))
|
||||
.on("rollback-service", this.rollbackService.bind(this));
|
||||
$("#btn-delete").click(this.deleteServices.bind(this));
|
||||
}
|
||||
deleteService(e) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:eq(1)").text().trim();
|
||||
let name = $tr.find("td:eq(0)").text().trim();
|
||||
Modal.confirm(`Are you sure to remove service: <strong>${name}</strong>?`, "Delete service", (dlg, e) => {
|
||||
$ajax.post("delete", { names: name }).trigger(e.target).encoder("form").json(() => {
|
||||
$ajax.post(`${name}/delete`, { names: name }).trigger(e.target).encoder("form").json(() => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
deleteServices() {
|
||||
let names = this.table.selectedKeys();
|
||||
if (names.length == 0) {
|
||||
Modal.alert("Please select one or more items.");
|
||||
return;
|
||||
}
|
||||
Modal.confirm(`Are you sure to remove ${names.length} services?`, "Delete services", (dlg, e) => {
|
||||
$ajax.post("delete", { names: names.join(",") }).trigger(e.target).encoder("form").json(() => {
|
||||
this.table.selectedRows().remove();
|
||||
dlg.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
scaleService(e) {
|
||||
let $btn = $(e.target).closest("button");
|
||||
let $tr = $btn.closest("tr");
|
||||
let data = {
|
||||
name: $tr.find("td:eq(1)").text().trim(),
|
||||
name: $tr.find("td:eq(0)").text().trim(),
|
||||
count: $btn.data("replicas"),
|
||||
};
|
||||
Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", (dlg, e) => {
|
||||
data.count = dlg.find("input[name=count]").val();
|
||||
$ajax.post("scale", data).trigger(e.target).encoder("form").json(() => {
|
||||
$ajax.post(`${data.name}/scale`, data).trigger(e.target).encoder("form").json(() => {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
rollbackService(e) {
|
||||
let $btn = $(e.target).closest("button"), $tr = $btn.closest("tr"), name = $tr.find("td:eq(1)").text().trim();
|
||||
let $btn = $(e.target).closest("button"), $tr = $btn.closest("tr"), name = $tr.find("td:eq(0)").text().trim();
|
||||
Modal.confirm(`Are you sure to rollback service: <strong>${name}</strong>?`, "Rollback service", (dlg, e) => {
|
||||
$ajax.post("rollback", { name: name }).trigger(e.target).encoder("form").json(() => {
|
||||
$ajax.post(`${name}/rollback`, { name: name }).trigger(e.target).encoder("form").json(() => {
|
||||
dlg.close();
|
||||
});
|
||||
});
|
||||
|
File diff suppressed because one or more lines are too long
61
assets/swirl/ts/perm/edit.ts
Normal file
61
assets/swirl/ts/perm/edit.ts
Normal file
@ -0,0 +1,61 @@
|
||||
///<reference path="../core/core.ts" />
|
||||
namespace Swirl.Perm {
|
||||
import Dispatcher = Swirl.Core.Dispatcher;
|
||||
import Modal = Swirl.Core.Modal;
|
||||
|
||||
export class EditPage {
|
||||
constructor() {
|
||||
// bind events
|
||||
$("#txt-query").keydown(this.searchUser);
|
||||
$("#btn-add-user").click(this.addUser);
|
||||
Dispatcher.bind("#div-users").on("delete-user", this.deleteUser.bind(this));
|
||||
}
|
||||
|
||||
private deleteUser(e: JQueryEventObject) {
|
||||
$(e.target).closest("div.control").remove();
|
||||
}
|
||||
|
||||
private searchUser(e: JQueryEventObject) {
|
||||
if (e.keyCode == 13) {
|
||||
let query = $.trim($(e.target).val());
|
||||
if (query.length == 0) {
|
||||
return;
|
||||
}
|
||||
$ajax.post("/system/user/search", {query: query}).encoder("form").json((users: any) => {
|
||||
let $panel = $("#nav-users");
|
||||
$panel.find("label.panel-block").remove();
|
||||
for (let user of users) {
|
||||
$panel.append(`<label class="panel-block">
|
||||
<input type="checkbox" value="${user.id}" data-name="${user.name}"> ${user.name}
|
||||
</label>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private addUser() {
|
||||
let users: { [index: string]: boolean } = {};
|
||||
$("#div-users").find("input").each((i, e) => {
|
||||
users[$(e).val()] = true;
|
||||
});
|
||||
|
||||
let $panel = $("#nav-users");
|
||||
$panel.find("input:checked").each((i, e) => {
|
||||
let $el = $(e);
|
||||
if (users[$el.val()]) {
|
||||
return;
|
||||
}
|
||||
|
||||
$("#div-users").append(`<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-info">${$el.data("name")}</span>
|
||||
<a class="tag is-delete" data-action="delete-user"></a>
|
||||
<input name="users[]" value="${$el.val()}" type="hidden">
|
||||
</div>`);
|
||||
});
|
||||
|
||||
Modal.close();
|
||||
$panel.find("label.panel-block").remove();
|
||||
}
|
||||
}
|
||||
}
|
@ -14,45 +14,29 @@ namespace Swirl.Service {
|
||||
this.table.on("delete-service", this.deleteService.bind(this))
|
||||
.on("scale-service", this.scaleService.bind(this))
|
||||
.on("rollback-service", this.rollbackService.bind(this));
|
||||
$("#btn-delete").click(this.deleteServices.bind(this));
|
||||
}
|
||||
|
||||
private deleteService(e: JQueryEventObject) {
|
||||
let $tr = $(e.target).closest("tr");
|
||||
let name = $tr.find("td:eq(1)").text().trim();
|
||||
let name = $tr.find("td:eq(0)").text().trim();
|
||||
Modal.confirm(`Are you sure to remove service: <strong>${name}</strong>?`, "Delete service", (dlg, e) => {
|
||||
$ajax.post("delete", {names: name}).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
$ajax.post(`${name}/delete`, {names: name}).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
$tr.remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private deleteServices() {
|
||||
let names = this.table.selectedKeys();
|
||||
if (names.length == 0) {
|
||||
Modal.alert("Please select one or more items.");
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm(`Are you sure to remove ${names.length} services?`, "Delete services", (dlg, e) => {
|
||||
$ajax.post("delete", {names: names.join(",")}).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
this.table.selectedRows().remove();
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private scaleService(e: JQueryEventObject) {
|
||||
let $btn = $(e.target).closest("button");
|
||||
let $tr = $btn.closest("tr");
|
||||
let data = {
|
||||
name: $tr.find("td:eq(1)").text().trim(),
|
||||
name: $tr.find("td:eq(0)").text().trim(),
|
||||
count: $btn.data("replicas"),
|
||||
};
|
||||
Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", (dlg, e) => {
|
||||
data.count = dlg.find("input[name=count]").val();
|
||||
$ajax.post("scale", data).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
$ajax.post(`${data.name}/scale`, data).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
location.reload();
|
||||
})
|
||||
});
|
||||
@ -61,9 +45,9 @@ namespace Swirl.Service {
|
||||
private rollbackService(e: JQueryEventObject) {
|
||||
let $btn = $(e.target).closest("button"),
|
||||
$tr = $btn.closest("tr"),
|
||||
name = $tr.find("td:eq(1)").text().trim();
|
||||
name = $tr.find("td:eq(0)").text().trim();
|
||||
Modal.confirm(`Are you sure to rollback service: <strong>${name}</strong>?`, "Rollback service", (dlg, e) => {
|
||||
$ajax.post("rollback", {name: name}).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
$ajax.post(`${name}/rollback`, {name: name}).trigger(e.target).encoder("form").json<AjaxResult>(() => {
|
||||
dlg.close();
|
||||
})
|
||||
});
|
||||
|
87
biz/perm.go
Normal file
87
biz/perm.go
Normal file
@ -0,0 +1,87 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/dao"
|
||||
"github.com/cuigh/swirl/model"
|
||||
)
|
||||
|
||||
// Perm return a perm biz instance.
|
||||
var (
|
||||
Perm = &permBiz{}
|
||||
ErrForbidden = web.NewError(http.StatusForbidden)
|
||||
)
|
||||
|
||||
type permBiz struct {
|
||||
}
|
||||
|
||||
func (b *permBiz) Delete(resType, resID string, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
err = d.PermDelete(resType, resID)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *permBiz) Get(resType, resID string) (perm *model.Perm, err error) {
|
||||
do(func(d dao.Interface) {
|
||||
perm, err = d.PermGet(resType, resID)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *permBiz) Update(perm *model.Perm, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
err = d.PermUpdate(perm)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *permBiz) Check(user web.User, scope string, resType, resID string) (err error) {
|
||||
au := user.(*model.AuthUser)
|
||||
if au.Admin() {
|
||||
return
|
||||
}
|
||||
|
||||
do(func(d dao.Interface) {
|
||||
var perm *model.Perm
|
||||
perm, err = d.PermGet(resType, resID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if perm == nil || perm.Scope == model.PermNone || (scope == "read" && perm.Scope == model.PermWrite) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range perm.Users {
|
||||
if user.ID() == u {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range perm.Roles {
|
||||
if au.IsInRole(r) {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = ErrForbidden
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *permBiz) Apply(next web.HandlerFunc) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
opt := ctx.Handler().Option("perm")
|
||||
if opt != "" {
|
||||
array := strings.Split(opt, ",")
|
||||
err := b.Check(ctx.User(), array[0], array[1], ctx.P(array[2]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/data/guid"
|
||||
@ -40,6 +41,34 @@ func (b *templateBiz) Get(id string) (tpl *model.Template, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *templateBiz) FillInfo(id string, si *model.ServiceInfo) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
var (
|
||||
tpl *model.Template
|
||||
registry *model.Registry
|
||||
)
|
||||
|
||||
tpl, err = d.TemplateGet(id)
|
||||
if err != nil || tpl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(tpl.Content), si)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if si.Registry != "" {
|
||||
registry, err = Registry.Get(si.Registry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
si.RegistryURL = registry.URL
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (b *templateBiz) Delete(id string, user web.User) (err error) {
|
||||
do(func(d dao.Interface) {
|
||||
var tpl *model.Template
|
||||
|
@ -12,6 +12,7 @@ button.confirm: Confirm
|
||||
button.delete: Delete
|
||||
button.prune: Prune
|
||||
button.new: New
|
||||
button.add: Add
|
||||
button.edit: Edit
|
||||
button.save: Save
|
||||
button.update: Update
|
||||
@ -42,6 +43,8 @@ field.image: Image
|
||||
field.address: Address
|
||||
field.driver: Driver
|
||||
field.scope: Scope
|
||||
field.role: Roles
|
||||
field.user: Users
|
||||
|
||||
# menu
|
||||
menu.dashboard: Dashboard
|
||||
@ -73,6 +76,7 @@ menu.detail: Detail
|
||||
menu.raw: Raw
|
||||
menu.edit: Edit
|
||||
menu.log: Logs
|
||||
menu.perm: Permission
|
||||
|
||||
# login page
|
||||
login.title: Sign in to Swirl
|
||||
|
@ -12,6 +12,7 @@ button.confirm: 确定
|
||||
button.delete: 删除
|
||||
button.prune: 清理
|
||||
button.new: 新建
|
||||
button.add: 添加
|
||||
button.edit: 编辑
|
||||
button.save: 保存
|
||||
button.update: 更新
|
||||
@ -42,6 +43,8 @@ field.image: 镜像
|
||||
field.address: 地址
|
||||
field.driver: 驱动
|
||||
field.scope: 范围
|
||||
field.role: 角色
|
||||
field.user: 用户
|
||||
|
||||
# menu
|
||||
menu.dashboard: 仪表盘
|
||||
@ -73,6 +76,7 @@ menu.detail: 详情
|
||||
menu.raw: 原始
|
||||
menu.edit: 编辑
|
||||
menu.log: 日志
|
||||
menu.perm: 权限
|
||||
|
||||
# login page
|
||||
login.title: 登录到 Swirl
|
||||
|
57
controller/perm.go
Normal file
57
controller/perm.go
Normal file
@ -0,0 +1,57 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/model"
|
||||
)
|
||||
|
||||
func permEdit(ctx web.Context, resType, resID, tpl string, m data.Map) error {
|
||||
perm, err := biz.Perm.Get(resType, resID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if perm == nil {
|
||||
perm = &model.Perm{}
|
||||
}
|
||||
|
||||
roles, err := biz.Role.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkedRoles := data.Set{}
|
||||
checkedRoles.AddSlice(perm.Roles, func(i int) interface{} {
|
||||
return perm.Roles[i]
|
||||
})
|
||||
|
||||
var users []*model.User
|
||||
for _, id := range perm.Users {
|
||||
var user *model.User
|
||||
if user, err = biz.User.GetByID(id); err != nil {
|
||||
return err
|
||||
} else if user != nil {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
|
||||
m.Set("Perm", perm).Set("Roles", roles).Set("CheckedRoles", checkedRoles).Set("Users", users)
|
||||
return ctx.Render(tpl, m)
|
||||
}
|
||||
|
||||
func permUpdate(resType, argName string) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
perm := &model.Perm{
|
||||
ResType: resType,
|
||||
ResID: ctx.P(argName),
|
||||
}
|
||||
err := ctx.Bind(perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = biz.Perm.Update(perm, ctx.User())
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -17,33 +16,37 @@ import (
|
||||
|
||||
// ServiceController is a controller of docker service
|
||||
type ServiceController struct {
|
||||
List web.HandlerFunc `path:"/" name:"service.list" authorize:"!" desc:"service list page"`
|
||||
Detail web.HandlerFunc `path:"/:name/detail" name:"service.detail" authorize:"!" desc:"service detail page"`
|
||||
Raw web.HandlerFunc `path:"/:name/raw" name:"service.raw" authorize:"!" desc:"service raw page"`
|
||||
Logs web.HandlerFunc `path:"/:name/logs" name:"service.logs" authorize:"!" desc:"service logs page"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" name:"service.delete" authorize:"!" desc:"delete service"`
|
||||
Scale web.HandlerFunc `path:"/scale" method:"post" name:"service.scale" authorize:"!" desc:"scale service"`
|
||||
Rollback web.HandlerFunc `path:"/rollback" method:"post" name:"service.rollback" authorize:"!" desc:"rollback service"`
|
||||
New web.HandlerFunc `path:"/new" name:"service.new" authorize:"!" desc:"new service page"`
|
||||
Create web.HandlerFunc `path:"/new" method:"post" name:"service.create" authorize:"!" desc:"create service"`
|
||||
Edit web.HandlerFunc `path:"/:name/edit" name:"service.edit" authorize:"!" desc:"service edit page"`
|
||||
Update web.HandlerFunc `path:"/:name/edit" method:"post" name:"service.update" authorize:"!" desc:"update service"`
|
||||
List web.HandlerFunc `path:"/" name:"service.list" authorize:"!" desc:"service list page"`
|
||||
Detail web.HandlerFunc `path:"/:name/detail" name:"service.detail" authorize:"!" perm:"read,service,name"`
|
||||
Raw web.HandlerFunc `path:"/:name/raw" name:"service.raw" authorize:"!" perm:"read,service,name"`
|
||||
Logs web.HandlerFunc `path:"/:name/logs" name:"service.logs" authorize:"!" perm:"read,service,name"`
|
||||
Delete web.HandlerFunc `path:"/:name/delete" method:"post" name:"service.delete" authorize:"!" perm:"write,service,name"`
|
||||
Scale web.HandlerFunc `path:"/:name/scale" method:"post" name:"service.scale" authorize:"!" perm:"write,service,name"`
|
||||
Rollback web.HandlerFunc `path:"/:name/rollback" method:"post" name:"service.rollback" authorize:"!" perm:"write,service,name"`
|
||||
New web.HandlerFunc `path:"/new" name:"service.new" authorize:"!" desc:"new service page"`
|
||||
Create web.HandlerFunc `path:"/new" method:"post" name:"service.create" authorize:"!" desc:"create service"`
|
||||
Edit web.HandlerFunc `path:"/:name/edit" name:"service.edit" authorize:"!" perm:"write,service,name"`
|
||||
Update web.HandlerFunc `path:"/:name/edit" method:"post" name:"service.update" authorize:"!" perm:"write,service,name"`
|
||||
PermEdit web.HandlerFunc `path:"/:name/perm" name:"service.perm.edit" authorize:"!" perm:"write,service,name"`
|
||||
PermUpdate web.HandlerFunc `path:"/:name/perm" method:"post" name:"service.perm.update" authorize:"!" perm:"write,service,name"`
|
||||
}
|
||||
|
||||
// Service creates an instance of ServiceController
|
||||
func Service() (c *ServiceController) {
|
||||
return &ServiceController{
|
||||
List: serviceList,
|
||||
Detail: serviceDetail,
|
||||
Raw: serviceRaw,
|
||||
Logs: serviceLogs,
|
||||
Delete: serviceDelete,
|
||||
New: serviceNew,
|
||||
Create: serviceCreate,
|
||||
Edit: serviceEdit,
|
||||
Update: serviceUpdate,
|
||||
Scale: serviceScale,
|
||||
Rollback: serviceRollback,
|
||||
List: serviceList,
|
||||
Detail: serviceDetail,
|
||||
Raw: serviceRaw,
|
||||
Logs: serviceLogs,
|
||||
Delete: serviceDelete,
|
||||
New: serviceNew,
|
||||
Create: serviceCreate,
|
||||
Edit: serviceEdit,
|
||||
Update: serviceUpdate,
|
||||
Scale: serviceScale,
|
||||
Rollback: serviceRollback,
|
||||
PermEdit: servicePermEdit,
|
||||
PermUpdate: permUpdate("service", "name"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,37 +129,20 @@ func serviceDelete(ctx web.Context) error {
|
||||
for _, name := range names {
|
||||
if err := docker.ServiceRemove(name); err != nil {
|
||||
return ajaxResult(ctx, err)
|
||||
} else {
|
||||
biz.Event.CreateService(model.EventActionDelete, name, ctx.User())
|
||||
}
|
||||
biz.Event.CreateService(model.EventActionDelete, name, ctx.User())
|
||||
}
|
||||
return ajaxSuccess(ctx, nil)
|
||||
}
|
||||
|
||||
func serviceNew(ctx web.Context) error {
|
||||
service := &model.ServiceInfo{}
|
||||
info := &model.ServiceInfo{}
|
||||
tid := ctx.Q("template")
|
||||
if tid != "" {
|
||||
tpl, err := biz.Template.Get(tid)
|
||||
err := biz.Template.FillInfo(tid, info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tpl != nil {
|
||||
err = json.Unmarshal([]byte(tpl.Content), service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if service.Registry != "" {
|
||||
var registry *model.Registry
|
||||
registry, err = biz.Registry.Get(service.Registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service.RegistryURL = registry.URL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
networks, err := docker.NetworkList()
|
||||
@ -177,11 +163,11 @@ func serviceNew(ctx web.Context) error {
|
||||
}
|
||||
|
||||
checkedNetworks := data.NewSet()
|
||||
checkedNetworks.AddSlice(service.Networks, func(i int) interface{} {
|
||||
return service.Networks[i]
|
||||
checkedNetworks.AddSlice(info.Networks, func(i int) interface{} {
|
||||
return info.Networks[i]
|
||||
})
|
||||
|
||||
m := newModel(ctx).Set("Service", service).Set("Registries", registries).
|
||||
m := newModel(ctx).Set("Service", info).Set("Registries", registries).
|
||||
Set("Networks", networks).Set("CheckedNetworks", checkedNetworks).
|
||||
Set("Secrets", secrets).Set("Configs", configs)
|
||||
return ctx.Render("service/new", m)
|
||||
@ -279,3 +265,9 @@ func serviceRollback(ctx web.Context) error {
|
||||
}
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
|
||||
func servicePermEdit(ctx web.Context) error {
|
||||
name := ctx.P("name")
|
||||
m := newModel(ctx).Set("Name", name)
|
||||
return permEdit(ctx, "service", name, "service/perm", m)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ type UserController struct {
|
||||
Block web.HandlerFunc `path:"/block" method:"post" name:"user.block" authorize:"!" desc:"block user"`
|
||||
Unblock web.HandlerFunc `path:"/unblock" method:"post" name:"user.unblock" authorize:"!" desc:"unblock user"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" name:"user.delete" authorize:"!" desc:"delete user"`
|
||||
Search web.HandlerFunc `path:"/search" method:"post" name:"user.search" authorize:"?" desc:"search users"`
|
||||
}
|
||||
|
||||
// User creates an instance of UserController
|
||||
@ -32,6 +33,7 @@ func User() (c *UserController) {
|
||||
Block: userBlock,
|
||||
Unblock: userUnblock,
|
||||
Delete: userDelete,
|
||||
Search: userSearch,
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,3 +161,29 @@ func userDelete(ctx web.Context) error {
|
||||
err := biz.User.Delete(id)
|
||||
return ajaxResult(ctx, err)
|
||||
}
|
||||
|
||||
func userSearch(ctx web.Context) error {
|
||||
query := ctx.F("query")
|
||||
args := &model.UserListArgs{
|
||||
Query: query,
|
||||
PageIndex: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
users, _, err := biz.User.List(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
list := make([]User, len(users))
|
||||
for i, user := range users {
|
||||
list[i] = User{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
}
|
||||
}
|
||||
return ctx.JSON(list)
|
||||
}
|
||||
|
@ -56,6 +56,10 @@ type Interface interface {
|
||||
EventCreate(event *model.Event) error
|
||||
EventList(args *model.EventListArgs) (events []*model.Event, count int, err error)
|
||||
|
||||
PermGet(resType, resID string) (*model.Perm, error)
|
||||
PermUpdate(perm *model.Perm) error
|
||||
PermDelete(resType, resID string) error
|
||||
|
||||
SettingGet() (setting *model.Setting, err error)
|
||||
SettingUpdate(setting *model.Setting) error
|
||||
}
|
||||
|
@ -35,6 +35,9 @@ var (
|
||||
"template": {
|
||||
mgo.Index{Key: []string{"name"}, Unique: true},
|
||||
},
|
||||
"perm": {
|
||||
mgo.Index{Key: []string{"res_type", "res_id"}, Unique: true},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
53
dao/mongo/perm.go
Normal file
53
dao/mongo/perm.go
Normal file
@ -0,0 +1,53 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"github.com/cuigh/swirl/model"
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
func (d *Dao) PermGet(resType, resID string) (p *model.Perm, err error) {
|
||||
d.do(func(db *database) {
|
||||
p = &model.Perm{}
|
||||
q := bson.M{
|
||||
"res_type": resType,
|
||||
"res_id": resID,
|
||||
}
|
||||
err = db.C("perm").Find(q).One(p)
|
||||
if err == mgo.ErrNotFound {
|
||||
p, err = nil, nil
|
||||
} else if err != nil {
|
||||
p = nil
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) PermUpdate(perm *model.Perm) (err error) {
|
||||
d.do(func(db *database) {
|
||||
q := bson.M{
|
||||
"res_type": perm.ResType,
|
||||
"res_id": perm.ResID,
|
||||
}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"scope": perm.Scope,
|
||||
"roles": perm.Roles,
|
||||
"users": perm.Users,
|
||||
},
|
||||
}
|
||||
_, err = db.C("perm").Upsert(q, update)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) PermDelete(resType, resID string) (err error) {
|
||||
d.do(func(db *database) {
|
||||
q := bson.M{
|
||||
"res_type": resType,
|
||||
"res_id": resID,
|
||||
}
|
||||
err = db.C("perm").Remove(q)
|
||||
})
|
||||
return
|
||||
}
|
4
main.go
4
main.go
@ -24,7 +24,7 @@ func main() {
|
||||
misc.BindOptions()
|
||||
|
||||
app.Name = "Swirl"
|
||||
app.Version = "0.6.3"
|
||||
app.Version = "0.6.4"
|
||||
app.Desc = "A web management UI for Docker, focused on swarm cluster"
|
||||
app.Action = func(ctx *app.Context) {
|
||||
misc.LoadOptions()
|
||||
@ -91,7 +91,7 @@ func server() *web.Server {
|
||||
g.Handle("/profile", controller.Profile())
|
||||
g.Handle("/registry", controller.Registry())
|
||||
g.Handle("/node", controller.Node())
|
||||
g.Handle("/service", controller.Service())
|
||||
g.Handle("/service", controller.Service(), biz.Perm)
|
||||
g.Handle("/service/template", controller.Template())
|
||||
g.Handle("/stack", controller.Stack())
|
||||
g.Handle("/network", controller.Network())
|
||||
|
@ -65,6 +65,7 @@ type Session struct {
|
||||
|
||||
type AuthUser struct {
|
||||
user *User
|
||||
roles []*Role
|
||||
perms map[string]struct{}
|
||||
}
|
||||
|
||||
@ -74,6 +75,7 @@ func NewAuthUser(user *User, roles []*Role) *AuthUser {
|
||||
}
|
||||
u := &AuthUser{
|
||||
user: user,
|
||||
roles: roles,
|
||||
perms: make(map[string]struct{}),
|
||||
}
|
||||
for _, role := range roles {
|
||||
@ -100,6 +102,15 @@ func (u *AuthUser) Admin() bool {
|
||||
return u.user.Admin
|
||||
}
|
||||
|
||||
func (u *AuthUser) IsInRole(roleID string) bool {
|
||||
for _, role := range u.roles {
|
||||
if role.ID == roleID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *AuthUser) IsAllowed(perm string) bool {
|
||||
if u.user.Admin {
|
||||
return true
|
||||
|
@ -2,17 +2,26 @@ package model
|
||||
|
||||
import "time"
|
||||
|
||||
// LDAP security policy
|
||||
const (
|
||||
LDAPSecurityNone = 0
|
||||
LDAPSecurityTLS = 1
|
||||
LDAPSecurityStartTLS = 2
|
||||
)
|
||||
|
||||
// LDAP auth type
|
||||
const (
|
||||
LDAPAuthSimple = 0
|
||||
LDAPAuthBind = 1
|
||||
)
|
||||
|
||||
// Perm control scope
|
||||
const (
|
||||
PermNone = 0
|
||||
PermWrite = 1
|
||||
PermReadWrite = 2
|
||||
)
|
||||
|
||||
// Setting represents the options of swirl.
|
||||
type Setting struct {
|
||||
LDAP struct {
|
||||
@ -38,3 +47,12 @@ type Setting struct {
|
||||
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`
|
||||
UpdatedAt time.Time `bson:"updated_at" json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// Perm holds permissions of Docker resource.
|
||||
type Perm struct {
|
||||
ResType string `json:"res_type"`
|
||||
ResID string `json:"res_id"`
|
||||
Scope int32 `json:"scope"`
|
||||
Roles []string `json:"roles"`
|
||||
Users []string `json:"users"`
|
||||
}
|
@ -29,6 +29,7 @@
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/raw">{{ i18n("menu.raw") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/logs">{{ i18n("menu.log") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -29,6 +29,7 @@
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service.Name}}/raw">{{ i18n("menu.raw") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service.Name}}/logs">{{ i18n("menu.log") }}</a>
|
||||
<a class="navbar-item is-tab is-active" href="/service/{{.Service.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -31,9 +31,6 @@
|
||||
</div>
|
||||
<!-- Right side -->
|
||||
<div class="level-right">
|
||||
<p class="level-item">
|
||||
<button id="btn-delete" class="button is-danger"><span class="icon"><i class="fa fa-remove"></i></span><span>{{ i18n("button.delete") }}</span></button>
|
||||
</p>
|
||||
<p class="level-item">
|
||||
<a class="button is-success" href="new"><span class="icon"><i class="fa fa-plus"></i></span><span>{{ i18n("button.new") }}</span></a>
|
||||
</p>
|
||||
@ -43,7 +40,6 @@
|
||||
<table id="table-items" class="table is-bordered is-striped is-narrow is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="30"><input type="checkbox" data-action="check-all"></th>
|
||||
<th>{{ i18n("field.name") }}</th>
|
||||
<th>{{ i18n("field.image") }}</th>
|
||||
<th width="145">Mode</th>
|
||||
@ -54,7 +50,6 @@
|
||||
<tbody>
|
||||
{{range .Services}}
|
||||
<tr>
|
||||
<td><input type="checkbox" value="{{.Name}}" data-action="check"></td>
|
||||
<td><a href="{{.Name}}/detail">{{.Name}}</a></td>
|
||||
<td>{{ limit(.Image, 60) }}</td>
|
||||
<td>
|
||||
|
@ -28,6 +28,7 @@
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/raw">{{ i18n("menu.raw") }}</a>
|
||||
<a class="navbar-item is-tab is-active" href="/service/{{.Service}}/logs">{{ i18n("menu.log") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/edit">{{ i18n("menu.edit") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/perm">{{ i18n("menu.perm") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
104
views/service/perm.jet
Normal file
104
views/service/perm.jet
Normal file
@ -0,0 +1,104 @@
|
||||
{{ extends "_base" }}
|
||||
{{ import "../_modules/form" }}
|
||||
|
||||
{{ block script() }}
|
||||
<script>$(() => new Swirl.Perm.EditPage())</script>
|
||||
{{ end }}
|
||||
|
||||
{{ block body_content() }}
|
||||
<div class="container">
|
||||
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="/">{{ i18n("menu.dashboard") }}</a></li>
|
||||
<li><a href="/service/">{{ i18n("menu.service") }}</a></li>
|
||||
<li class="is-active"><a>{{ i18n("menu.perm") }}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<section class="hero is-small is-light">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h2 class="title is-2">{{ .Name }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="navbar has-shadow">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item is-tab" href="/service/{{.Name}}/detail">{{ i18n("menu.detail") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Name}}/raw">{{ i18n("menu.raw") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Name}}/logs">{{ i18n("menu.log") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
||||
<a class="navbar-item is-tab is-active" href="/service/{{.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<form method="post" data-form="ajax-json" data-url="/service/">
|
||||
<div class="field">
|
||||
<label class="label">{{ i18n("field.scope") }}</label>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
{{ yield radio(name="scope", value=0, label="None", checked=.Perm.Scope) content }} data-type="integer"{{ end }}
|
||||
{{ yield radio(name="scope", value=1, label="Write", checked=.Perm.Scope) content }} data-type="integer"{{ end }}
|
||||
{{ yield radio(name="scope", value=2, label="Read & Write", checked=.Perm.Scope) content }} data-type="integer"{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ i18n("field.role") }}</label>
|
||||
<div class="control">
|
||||
{{ roles := .CheckedRoles }}
|
||||
{{range .Roles}}
|
||||
{{ yield checkbox(name="roles", value=.ID, label=.Name, checked=isset(roles[.ID])) }}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" style="display: inline-block">{{ i18n("field.user") }}</label>
|
||||
<button type="button" class="button is-small is-success is-outlined tooltip is-tooltip-bottom modal-trigger" data-tooltip="{{ i18n("button.add") }}" data-action="add-user" data-target="dlg-add-user">
|
||||
<span class="icon"><i class="fa fa-plus"></i></span>
|
||||
</button>
|
||||
<div id="div-users" class="field is-grouped is-grouped-multiline">
|
||||
{{range .Users}}
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-info">{{ .Name }}</span>
|
||||
<a class="tag is-delete" data-action="delete-user"></a>
|
||||
<input name="users[]" value="{{ .ID }}" type="hidden">
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{ yield form_submit(url="/service/") }}
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<div id="dlg-add-user" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Add user</p>
|
||||
<button class="delete"></button>
|
||||
</header>
|
||||
<section class="modal-card-body" style="max-height: 400px; overflow-y: auto">
|
||||
<nav id="nav-users" class="panel">
|
||||
<div class="panel-block">
|
||||
<p class="control has-icons-left">
|
||||
<input id="txt-query" class="input is-small" type="text" placeholder="Searching user...">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fa fa-search"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button id="btn-add-user" type="button" class="button is-primary">{{ i18n("button.confirm") }}</button>
|
||||
<button type="button" class="button dismiss">{{ i18n("button.cancel") }}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
@ -37,6 +37,7 @@
|
||||
<a class="navbar-item is-tab is-active" href="/service/{{.Service}}/raw">{{ i18n("menu.raw") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/logs">{{ i18n("menu.log") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/edit">{{ i18n("menu.edit") }}</a>
|
||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/perm">{{ i18n("menu.perm") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
Loading…
Reference in New Issue
Block a user