Add permission control for service resource

This commit is contained in:
cuigh 2017-11-24 13:03:51 +08:00
parent 8ce6adf478
commit 1f652bb6f3
23 changed files with 577 additions and 96 deletions

View File

@ -1350,6 +1350,63 @@ var Swirl;
})(Node = Swirl.Node || (Swirl.Node = {})); })(Node = Swirl.Node || (Swirl.Node = {}));
})(Swirl || (Swirl = {})); })(Swirl || (Swirl = {}));
var 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) { (function (Swirl) {
var Registry; var Registry;
(function (Registry) { (function (Registry) {
@ -1762,49 +1819,35 @@ var Swirl;
this.table.on("delete-service", this.deleteService.bind(this)) this.table.on("delete-service", this.deleteService.bind(this))
.on("scale-service", this.scaleService.bind(this)) .on("scale-service", this.scaleService.bind(this))
.on("rollback-service", this.rollbackService.bind(this)); .on("rollback-service", this.rollbackService.bind(this));
$("#btn-delete").click(this.deleteServices.bind(this));
} }
deleteService(e) { deleteService(e) {
let $tr = $(e.target).closest("tr"); 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) => { 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(); $tr.remove();
dlg.close(); 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) { scaleService(e) {
let $btn = $(e.target).closest("button"); let $btn = $(e.target).closest("button");
let $tr = $btn.closest("tr"); let $tr = $btn.closest("tr");
let data = { let data = {
name: $tr.find("td:eq(1)").text().trim(), name: $tr.find("td:eq(0)").text().trim(),
count: $btn.data("replicas"), count: $btn.data("replicas"),
}; };
Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", (dlg, e) => { Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", (dlg, e) => {
data.count = dlg.find("input[name=count]").val(); 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(); location.reload();
}); });
}); });
} }
rollbackService(e) { 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) => { 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(); dlg.close();
}); });
}); });

File diff suppressed because one or more lines are too long

View 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();
}
}
}

View File

@ -14,45 +14,29 @@ namespace Swirl.Service {
this.table.on("delete-service", this.deleteService.bind(this)) this.table.on("delete-service", this.deleteService.bind(this))
.on("scale-service", this.scaleService.bind(this)) .on("scale-service", this.scaleService.bind(this))
.on("rollback-service", this.rollbackService.bind(this)); .on("rollback-service", this.rollbackService.bind(this));
$("#btn-delete").click(this.deleteServices.bind(this));
} }
private deleteService(e: JQueryEventObject) { private deleteService(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr"); 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) => { 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(); $tr.remove();
dlg.close(); 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) { private scaleService(e: JQueryEventObject) {
let $btn = $(e.target).closest("button"); let $btn = $(e.target).closest("button");
let $tr = $btn.closest("tr"); let $tr = $btn.closest("tr");
let data = { let data = {
name: $tr.find("td:eq(1)").text().trim(), name: $tr.find("td:eq(0)").text().trim(),
count: $btn.data("replicas"), count: $btn.data("replicas"),
}; };
Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", (dlg, e) => { Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", (dlg, e) => {
data.count = dlg.find("input[name=count]").val(); 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(); location.reload();
}) })
}); });
@ -61,9 +45,9 @@ namespace Swirl.Service {
private rollbackService(e: JQueryEventObject) { private rollbackService(e: JQueryEventObject) {
let $btn = $(e.target).closest("button"), let $btn = $(e.target).closest("button"),
$tr = $btn.closest("tr"), $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) => { 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(); dlg.close();
}) })
}); });

87
biz/perm.go Normal file
View 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)
}
}

View File

@ -1,6 +1,7 @@
package biz package biz
import ( import (
"encoding/json"
"time" "time"
"github.com/cuigh/auxo/data/guid" "github.com/cuigh/auxo/data/guid"
@ -40,6 +41,34 @@ func (b *templateBiz) Get(id string) (tpl *model.Template, err error) {
return 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) { func (b *templateBiz) Delete(id string, user web.User) (err error) {
do(func(d dao.Interface) { do(func(d dao.Interface) {
var tpl *model.Template var tpl *model.Template

View File

@ -12,6 +12,7 @@ button.confirm: Confirm
button.delete: Delete button.delete: Delete
button.prune: Prune button.prune: Prune
button.new: New button.new: New
button.add: Add
button.edit: Edit button.edit: Edit
button.save: Save button.save: Save
button.update: Update button.update: Update
@ -42,6 +43,8 @@ field.image: Image
field.address: Address field.address: Address
field.driver: Driver field.driver: Driver
field.scope: Scope field.scope: Scope
field.role: Roles
field.user: Users
# menu # menu
menu.dashboard: Dashboard menu.dashboard: Dashboard
@ -73,6 +76,7 @@ menu.detail: Detail
menu.raw: Raw menu.raw: Raw
menu.edit: Edit menu.edit: Edit
menu.log: Logs menu.log: Logs
menu.perm: Permission
# login page # login page
login.title: Sign in to Swirl login.title: Sign in to Swirl

View File

@ -12,6 +12,7 @@ button.confirm: 确定
button.delete: 删除 button.delete: 删除
button.prune: 清理 button.prune: 清理
button.new: 新建 button.new: 新建
button.add: 添加
button.edit: 编辑 button.edit: 编辑
button.save: 保存 button.save: 保存
button.update: 更新 button.update: 更新
@ -42,6 +43,8 @@ field.image: 镜像
field.address: 地址 field.address: 地址
field.driver: 驱动 field.driver: 驱动
field.scope: 范围 field.scope: 范围
field.role: 角色
field.user: 用户
# menu # menu
menu.dashboard: 仪表盘 menu.dashboard: 仪表盘
@ -73,6 +76,7 @@ menu.detail: 详情
menu.raw: 原始 menu.raw: 原始
menu.edit: 编辑 menu.edit: 编辑
menu.log: 日志 menu.log: 日志
menu.perm: 权限
# login page # login page
login.title: 登录到 Swirl login.title: 登录到 Swirl

57
controller/perm.go Normal file
View 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)
}
}

View File

@ -1,7 +1,6 @@
package controller package controller
import ( import (
"encoding/json"
"strconv" "strconv"
"strings" "strings"
@ -18,16 +17,18 @@ import (
// ServiceController is a controller of docker service // ServiceController is a controller of docker service
type ServiceController struct { type ServiceController struct {
List web.HandlerFunc `path:"/" name:"service.list" authorize:"!" desc:"service list page"` 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"` Detail web.HandlerFunc `path:"/:name/detail" name:"service.detail" authorize:"!" perm:"read,service,name"`
Raw web.HandlerFunc `path:"/:name/raw" name:"service.raw" authorize:"!" desc:"service raw page"` Raw web.HandlerFunc `path:"/:name/raw" name:"service.raw" authorize:"!" perm:"read,service,name"`
Logs web.HandlerFunc `path:"/:name/logs" name:"service.logs" authorize:"!" desc:"service logs page"` Logs web.HandlerFunc `path:"/:name/logs" name:"service.logs" authorize:"!" perm:"read,service,name"`
Delete web.HandlerFunc `path:"/delete" method:"post" name:"service.delete" authorize:"!" desc:"delete service"` Delete web.HandlerFunc `path:"/:name/delete" method:"post" name:"service.delete" authorize:"!" perm:"write,service,name"`
Scale web.HandlerFunc `path:"/scale" method:"post" name:"service.scale" authorize:"!" desc:"scale service"` Scale web.HandlerFunc `path:"/:name/scale" method:"post" name:"service.scale" authorize:"!" perm:"write,service,name"`
Rollback web.HandlerFunc `path:"/rollback" method:"post" name:"service.rollback" authorize:"!" desc:"rollback service"` 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"` 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"` 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"` 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:"!" desc:"update service"` 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 // Service creates an instance of ServiceController
@ -44,6 +45,8 @@ func Service() (c *ServiceController) {
Update: serviceUpdate, Update: serviceUpdate,
Scale: serviceScale, Scale: serviceScale,
Rollback: serviceRollback, Rollback: serviceRollback,
PermEdit: servicePermEdit,
PermUpdate: permUpdate("service", "name"),
} }
} }
@ -126,37 +129,20 @@ func serviceDelete(ctx web.Context) error {
for _, name := range names { for _, name := range names {
if err := docker.ServiceRemove(name); err != nil { if err := docker.ServiceRemove(name); err != nil {
return ajaxResult(ctx, err) 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) return ajaxSuccess(ctx, nil)
} }
func serviceNew(ctx web.Context) error { func serviceNew(ctx web.Context) error {
service := &model.ServiceInfo{} info := &model.ServiceInfo{}
tid := ctx.Q("template") tid := ctx.Q("template")
if tid != "" { if tid != "" {
tpl, err := biz.Template.Get(tid) err := biz.Template.FillInfo(tid, info)
if err != nil { if err != nil {
return err 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() networks, err := docker.NetworkList()
@ -177,11 +163,11 @@ func serviceNew(ctx web.Context) error {
} }
checkedNetworks := data.NewSet() checkedNetworks := data.NewSet()
checkedNetworks.AddSlice(service.Networks, func(i int) interface{} { checkedNetworks.AddSlice(info.Networks, func(i int) interface{} {
return service.Networks[i] 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("Networks", networks).Set("CheckedNetworks", checkedNetworks).
Set("Secrets", secrets).Set("Configs", configs) Set("Secrets", secrets).Set("Configs", configs)
return ctx.Render("service/new", m) return ctx.Render("service/new", m)
@ -279,3 +265,9 @@ func serviceRollback(ctx web.Context) error {
} }
return ajaxResult(ctx, err) 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)
}

View File

@ -18,6 +18,7 @@ type UserController struct {
Block web.HandlerFunc `path:"/block" method:"post" name:"user.block" authorize:"!" desc:"block user"` 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"` 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"` 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 // User creates an instance of UserController
@ -32,6 +33,7 @@ func User() (c *UserController) {
Block: userBlock, Block: userBlock,
Unblock: userUnblock, Unblock: userUnblock,
Delete: userDelete, Delete: userDelete,
Search: userSearch,
} }
} }
@ -159,3 +161,29 @@ func userDelete(ctx web.Context) error {
err := biz.User.Delete(id) err := biz.User.Delete(id)
return ajaxResult(ctx, err) 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)
}

View File

@ -56,6 +56,10 @@ type Interface interface {
EventCreate(event *model.Event) error EventCreate(event *model.Event) error
EventList(args *model.EventListArgs) (events []*model.Event, count int, err 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) SettingGet() (setting *model.Setting, err error)
SettingUpdate(setting *model.Setting) error SettingUpdate(setting *model.Setting) error
} }

View File

@ -35,6 +35,9 @@ var (
"template": { "template": {
mgo.Index{Key: []string{"name"}, Unique: true}, 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
View 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
}

View File

@ -24,7 +24,7 @@ func main() {
misc.BindOptions() misc.BindOptions()
app.Name = "Swirl" 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.Desc = "A web management UI for Docker, focused on swarm cluster"
app.Action = func(ctx *app.Context) { app.Action = func(ctx *app.Context) {
misc.LoadOptions() misc.LoadOptions()
@ -91,7 +91,7 @@ func server() *web.Server {
g.Handle("/profile", controller.Profile()) g.Handle("/profile", controller.Profile())
g.Handle("/registry", controller.Registry()) g.Handle("/registry", controller.Registry())
g.Handle("/node", controller.Node()) 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("/service/template", controller.Template())
g.Handle("/stack", controller.Stack()) g.Handle("/stack", controller.Stack())
g.Handle("/network", controller.Network()) g.Handle("/network", controller.Network())

View File

@ -65,6 +65,7 @@ type Session struct {
type AuthUser struct { type AuthUser struct {
user *User user *User
roles []*Role
perms map[string]struct{} perms map[string]struct{}
} }
@ -74,6 +75,7 @@ func NewAuthUser(user *User, roles []*Role) *AuthUser {
} }
u := &AuthUser{ u := &AuthUser{
user: user, user: user,
roles: roles,
perms: make(map[string]struct{}), perms: make(map[string]struct{}),
} }
for _, role := range roles { for _, role := range roles {
@ -100,6 +102,15 @@ func (u *AuthUser) Admin() bool {
return u.user.Admin 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 { func (u *AuthUser) IsAllowed(perm string) bool {
if u.user.Admin { if u.user.Admin {
return true return true

View File

@ -2,17 +2,26 @@ package model
import "time" import "time"
// LDAP security policy
const ( const (
LDAPSecurityNone = 0 LDAPSecurityNone = 0
LDAPSecurityTLS = 1 LDAPSecurityTLS = 1
LDAPSecurityStartTLS = 2 LDAPSecurityStartTLS = 2
) )
// LDAP auth type
const ( const (
LDAPAuthSimple = 0 LDAPAuthSimple = 0
LDAPAuthBind = 1 LDAPAuthBind = 1
) )
// Perm control scope
const (
PermNone = 0
PermWrite = 1
PermReadWrite = 2
)
// Setting represents the options of swirl. // Setting represents the options of swirl.
type Setting struct { type Setting struct {
LDAP struct { LDAP struct {
@ -38,3 +47,12 @@ type Setting struct {
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"` UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at,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"`
}

View File

@ -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}}/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}}/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}}/edit">{{ i18n("menu.edit") }}</a>
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/perm">{{ i18n("menu.perm") }}</a>
</div> </div>
</div> </div>
</nav> </nav>

View File

@ -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}}/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" 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 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>
</div> </div>
</nav> </nav>

View File

@ -31,9 +31,6 @@
</div> </div>
<!-- Right side --> <!-- Right side -->
<div class="level-right"> <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"> <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> <a class="button is-success" href="new"><span class="icon"><i class="fa fa-plus"></i></span><span>{{ i18n("button.new") }}</span></a>
</p> </p>
@ -43,7 +40,6 @@
<table id="table-items" class="table is-bordered is-striped is-narrow is-fullwidth"> <table id="table-items" class="table is-bordered is-striped is-narrow is-fullwidth">
<thead> <thead>
<tr> <tr>
<th width="30"><input type="checkbox" data-action="check-all"></th>
<th>{{ i18n("field.name") }}</th> <th>{{ i18n("field.name") }}</th>
<th>{{ i18n("field.image") }}</th> <th>{{ i18n("field.image") }}</th>
<th width="145">Mode</th> <th width="145">Mode</th>
@ -54,7 +50,6 @@
<tbody> <tbody>
{{range .Services}} {{range .Services}}
<tr> <tr>
<td><input type="checkbox" value="{{.Name}}" data-action="check"></td>
<td><a href="{{.Name}}/detail">{{.Name}}</a></td> <td><a href="{{.Name}}/detail">{{.Name}}</a></td>
<td>{{ limit(.Image, 60) }}</td> <td>{{ limit(.Image, 60) }}</td>
<td> <td>

View File

@ -28,6 +28,7 @@
<a class="navbar-item is-tab" href="/service/{{.Service}}/raw">{{ i18n("menu.raw") }}</a> <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 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}}/edit">{{ i18n("menu.edit") }}</a>
<a class="navbar-item is-tab" href="/service/{{.Service}}/perm">{{ i18n("menu.perm") }}</a>
</div> </div>
</div> </div>
</nav> </nav>

104
views/service/perm.jet Normal file
View 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 }}

View File

@ -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 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}}/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}}/edit">{{ i18n("menu.edit") }}</a>
<a class="navbar-item is-tab" href="/service/{{.Service}}/perm">{{ i18n("menu.perm") }}</a>
</div> </div>
</div> </div>
</nav> </nav>