mirror of
https://github.com/cuigh/swirl
synced 2025-06-26 18:16:50 +00:00
Refactor authentication and authorization
This commit is contained in:
parent
dfe15524a2
commit
487d73d643
@ -35,4 +35,5 @@ func init() {
|
||||
container.Put(NewRole, container.Name("api.role"))
|
||||
container.Put(NewEvent, container.Name("api.event"))
|
||||
container.Put(NewChart, container.Name("api.chart"))
|
||||
container.Put(NewDashboard, container.Name("api.dashboard"))
|
||||
}
|
||||
|
80
api/chart.go
80
api/chart.go
@ -1,11 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/auxo/ext/times"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/model"
|
||||
@ -13,25 +9,19 @@ import (
|
||||
|
||||
// ChartHandler encapsulates chart related handlers.
|
||||
type ChartHandler struct {
|
||||
Search web.HandlerFunc `path:"/search" auth:"chart.view" desc:"search charts"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"chart.view" desc:"find chart by id"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"chart.edit" desc:"create or update chart"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"chart.delete" desc:"delete chart"`
|
||||
FetchData web.HandlerFunc `path:"/fetch-data" auth:"?" desc:"fetch chart data"`
|
||||
FindDashboard web.HandlerFunc `path:"/find-dashboard" auth:"?" desc:"find dashboard by name and key"`
|
||||
SaveDashboard web.HandlerFunc `path:"/save-dashboard" method:"post" auth:"chart.dashboard" desc:"save dashboard"`
|
||||
Search web.HandlerFunc `path:"/search" auth:"chart.view" desc:"search charts"`
|
||||
Find web.HandlerFunc `path:"/find" auth:"chart.view" desc:"find chart by id"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"chart.edit" desc:"create or update chart"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"chart.delete" desc:"delete chart"`
|
||||
}
|
||||
|
||||
// NewChart creates an instance of ChartHandler
|
||||
func NewChart(b biz.ChartBiz) *ChartHandler {
|
||||
return &ChartHandler{
|
||||
Search: chartSearch(b),
|
||||
Find: chartFind(b),
|
||||
Delete: chartDelete(b),
|
||||
Save: chartSave(b),
|
||||
FetchData: chartFetchData(b),
|
||||
FindDashboard: chartFindDashboard(b),
|
||||
SaveDashboard: chartSaveDashboard(b),
|
||||
Search: chartSearch(b),
|
||||
Find: chartFind(b),
|
||||
Delete: chartDelete(b),
|
||||
Save: chartSave(b),
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,57 +87,3 @@ func chartSave(b biz.ChartBiz) web.HandlerFunc {
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func chartFetchData(b biz.ChartBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Key string `json:"key" bind:"key"`
|
||||
Charts string `json:"charts" bind:"charts"`
|
||||
Period int32 `json:"period" bind:"period"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
d data.Map
|
||||
)
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
d, err = b.FetchData(args.Key, strings.Split(args.Charts, ","), times.Minutes(args.Period))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, d)
|
||||
}
|
||||
}
|
||||
|
||||
func chartFindDashboard(b biz.ChartBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
d *model.Dashboard
|
||||
name = ctx.Query("name")
|
||||
key = ctx.Query("key")
|
||||
)
|
||||
d, err = b.FindDashboard(name, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, d)
|
||||
}
|
||||
}
|
||||
|
||||
func chartSaveDashboard(b biz.ChartBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
dashboard := &model.Dashboard{}
|
||||
err := ctx.Bind(dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch dashboard.Name {
|
||||
case "home", "service":
|
||||
err = b.UpdateDashboard(dashboard, ctx.User())
|
||||
default:
|
||||
err = errors.New("unknown dashboard: " + dashboard.Name)
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ type ContainerHandler struct {
|
||||
Find web.HandlerFunc `path:"/find" auth:"container.view" desc:"find container by name"`
|
||||
Delete web.HandlerFunc `path:"/delete" method:"post" auth:"container.delete" desc:"delete container"`
|
||||
FetchLogs web.HandlerFunc `path:"/fetch-logs" auth:"container.logs" desc:"fetch logs of container"`
|
||||
Connect web.HandlerFunc `path:"/connect" auth:"*" desc:"connect to a running container"`
|
||||
Connect web.HandlerFunc `path:"/connect" auth:"container.execute" desc:"connect to a running container"`
|
||||
}
|
||||
|
||||
// NewContainer creates an instance of ContainerHandler
|
||||
|
82
api/dashboard.go
Normal file
82
api/dashboard.go
Normal file
@ -0,0 +1,82 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/auxo/ext/times"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/model"
|
||||
)
|
||||
|
||||
// DashboardHandler encapsulates dashboard related handlers.
|
||||
type DashboardHandler struct {
|
||||
Find web.HandlerFunc `path:"/find" auth:"?" desc:"find dashboard by name and key"`
|
||||
Save web.HandlerFunc `path:"/save" method:"post" auth:"dashboard.edit" desc:"save dashboard"`
|
||||
FetchData web.HandlerFunc `path:"/fetch-data" auth:"?" desc:"fetch data of dashboard charts"`
|
||||
}
|
||||
|
||||
// NewDashboard creates an instance of DashboardHandler
|
||||
func NewDashboard(b biz.DashboardBiz) *DashboardHandler {
|
||||
return &DashboardHandler{
|
||||
Find: dashboardFind(b),
|
||||
Save: dashboardSave(b),
|
||||
FetchData: dashboardFetchData(b),
|
||||
}
|
||||
}
|
||||
|
||||
func dashboardFind(b biz.DashboardBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
d *model.Dashboard
|
||||
name = ctx.Query("name")
|
||||
key = ctx.Query("key")
|
||||
)
|
||||
d, err = b.FindDashboard(name, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, d)
|
||||
}
|
||||
}
|
||||
|
||||
func dashboardSave(b biz.DashboardBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
dashboard := &model.Dashboard{}
|
||||
err := ctx.Bind(dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch dashboard.Name {
|
||||
case "home", "service":
|
||||
err = b.UpdateDashboard(dashboard, ctx.User())
|
||||
default:
|
||||
err = errors.New("unknown dashboard: " + dashboard.Name)
|
||||
}
|
||||
return ajax(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func dashboardFetchData(b biz.DashboardBiz) web.HandlerFunc {
|
||||
type Args struct {
|
||||
Key string `json:"key" bind:"key"`
|
||||
Dashboards string `json:"charts" bind:"charts"`
|
||||
Period int32 `json:"period" bind:"period"`
|
||||
}
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &Args{}
|
||||
d data.Map
|
||||
)
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
d, err = b.FetchData(args.Key, strings.Split(args.Dashboards, ","), times.Minutes(args.Period))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return success(ctx, d)
|
||||
}
|
||||
}
|
16
api/node.go
16
api/node.go
@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/auxo/util/cast"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
@ -28,22 +29,11 @@ func NewNode(nb biz.NodeBiz) *NodeHandler {
|
||||
|
||||
func nodeList(nb biz.NodeBiz) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
nodes, err := nb.List()
|
||||
agent := cast.ToBool(ctx.Query("agent"))
|
||||
nodes, err := nb.List(agent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Query("agent") == "true" {
|
||||
i := 0
|
||||
for j := 0; j < len(nodes); j++ {
|
||||
if nodes[j].Agent != "" {
|
||||
nodes[i] = nodes[j]
|
||||
i++
|
||||
}
|
||||
}
|
||||
nodes = nodes[:i]
|
||||
}
|
||||
|
||||
return success(ctx, nodes)
|
||||
}
|
||||
}
|
||||
|
24
api/user.go
24
api/user.go
@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/app/container"
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
@ -22,7 +21,7 @@ type UserHandler struct {
|
||||
}
|
||||
|
||||
// NewUser creates an instance of UserHandler
|
||||
func NewUser(b biz.UserBiz, eb biz.EventBiz, auth *security.Authenticator) *UserHandler {
|
||||
func NewUser(b biz.UserBiz, eb biz.EventBiz, auth *security.Identifier) *UserHandler {
|
||||
return &UserHandler{
|
||||
SignIn: userSignIn(auth, eb),
|
||||
Search: userSearch(b),
|
||||
@ -35,7 +34,7 @@ func NewUser(b biz.UserBiz, eb biz.EventBiz, auth *security.Authenticator) *User
|
||||
}
|
||||
}
|
||||
|
||||
func userSignIn(auth *security.Authenticator, eb biz.EventBiz) web.HandlerFunc {
|
||||
func userSignIn(auth *security.Identifier, eb biz.EventBiz) web.HandlerFunc {
|
||||
type SignInArgs struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
@ -43,27 +42,24 @@ func userSignIn(auth *security.Authenticator, eb biz.EventBiz) web.HandlerFunc {
|
||||
|
||||
return func(ctx web.Context) (err error) {
|
||||
var (
|
||||
args = &SignInArgs{}
|
||||
user web.User
|
||||
token string
|
||||
args = &SignInArgs{}
|
||||
user security.Identity
|
||||
)
|
||||
|
||||
if err = ctx.Bind(args); err == nil {
|
||||
if user, err = auth.Login(args.Name, args.Password); err == nil {
|
||||
jwt := container.Find("identifier").(*security.JWT)
|
||||
token, err = jwt.CreateToken(user.ID(), user.Name())
|
||||
}
|
||||
if err = ctx.Bind(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if user, err = auth.Identify(args.Name, args.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eb.CreateUser(biz.EventActionLogin, user.ID(), user.Name(), user)
|
||||
|
||||
return success(ctx, data.Map{
|
||||
"token": token,
|
||||
"id": user.ID(),
|
||||
"name": user.Name(),
|
||||
"token": user.Token(),
|
||||
"perms": user.Perms(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -130,4 +130,6 @@ func init() {
|
||||
container.Put(NewMetric)
|
||||
container.Put(NewChart)
|
||||
container.Put(NewSystem)
|
||||
container.Put(NewSession)
|
||||
container.Put(NewDashboard)
|
||||
}
|
||||
|
239
biz/chart.go
239
biz/chart.go
@ -2,25 +2,12 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/dao"
|
||||
"github.com/cuigh/swirl/model"
|
||||
"github.com/jinzhu/copier"
|
||||
)
|
||||
|
||||
var builtins = []*model.Chart{
|
||||
model.NewChart("service", "$cpu", "CPU", "${name}", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="${service}"}[5m]) * 100`, "percent:100", 60),
|
||||
model.NewChart("service", "$memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="${service}"}`, "size:bytes", 60),
|
||||
model.NewChart("service", "$network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes", 60),
|
||||
model.NewChart("service", "$network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes", 60),
|
||||
}
|
||||
|
||||
type ChartBiz interface {
|
||||
Search(args *model.ChartSearchArgs) (charts []*model.Chart, total int, err error)
|
||||
Delete(id, title string, user web.User) (err error)
|
||||
@ -28,9 +15,6 @@ type ChartBiz interface {
|
||||
Batch(ids ...string) (charts []*model.Chart, err error)
|
||||
Create(chart *model.Chart, user web.User) (err error)
|
||||
Update(chart *model.Chart, user web.User) (err error)
|
||||
FetchData(key string, ids []string, period time.Duration) (data.Map, error)
|
||||
FindDashboard(name, key string) (dashboard *model.Dashboard, err error)
|
||||
UpdateDashboard(dashboard *model.Dashboard, user web.User) (err error)
|
||||
}
|
||||
|
||||
func NewChart(d dao.Interface, mb MetricBiz, eb EventBiz) ChartBiz {
|
||||
@ -90,226 +74,3 @@ func (b *chartBiz) Update(chart *model.Chart, user web.User) (err error) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) FindDashboard(name, key string) (dashboard *model.Dashboard, err error) {
|
||||
if dashboard, err = b.d.DashboardGet(context.TODO(), name, key); err != nil {
|
||||
return
|
||||
}
|
||||
if dashboard == nil {
|
||||
dashboard = defaultDashboard(name, key)
|
||||
}
|
||||
err = b.fillCharts(dashboard)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) UpdateDashboard(dashboard *model.Dashboard, user web.User) (err error) {
|
||||
dashboard.UpdatedAt = now()
|
||||
dashboard.UpdatedBy = newOperator(user)
|
||||
return b.d.DashboardUpdate(context.TODO(), dashboard)
|
||||
}
|
||||
|
||||
func (b *chartBiz) FetchData(key string, ids []string, period time.Duration) (data.Map, error) {
|
||||
if !b.mb.Enabled() {
|
||||
return data.Map{}, nil
|
||||
}
|
||||
|
||||
charts, err := b.getCharts(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
id string
|
||||
data interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
ch := make(chan Data, len(charts))
|
||||
end := time.Now()
|
||||
start := end.Add(-period)
|
||||
for _, chart := range charts {
|
||||
go func(c *model.Chart) {
|
||||
d := Data{id: c.ID}
|
||||
switch c.Type {
|
||||
case "line", "bar":
|
||||
d.data, d.err = b.fetchMatrixData(c, key, start, end)
|
||||
case "pie":
|
||||
d.data, d.err = b.fetchVectorData(c, key, end)
|
||||
case "gauge":
|
||||
d.data, d.err = b.fetchScalarData(c, key, end)
|
||||
default:
|
||||
d.err = errors.New("invalid chart type: " + c.Type)
|
||||
}
|
||||
ch <- d
|
||||
}(chart)
|
||||
}
|
||||
|
||||
ds := data.Map{}
|
||||
for range charts {
|
||||
d := <-ch
|
||||
if d.err != nil {
|
||||
log.Get("metric").Error(d.err)
|
||||
} else {
|
||||
ds.Set(d.id, d.data)
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func (b *chartBiz) fetchMatrixData(chart *model.Chart, key string, start, end time.Time) (md *MatrixData, err error) {
|
||||
var (
|
||||
q string
|
||||
d *MatrixData
|
||||
)
|
||||
for i, m := range chart.Metrics {
|
||||
q, err = b.formatQuery(m.Query, chart.Dashboard, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d, err = b.mb.GetMatrix(q, m.Legend, start, end); err != nil {
|
||||
log.Get("metric").Error(err)
|
||||
} else if i == 0 {
|
||||
md = d
|
||||
} else {
|
||||
md.Legend = append(md.Legend, d.Legend...)
|
||||
md.Series = append(md.Series, d.Series...)
|
||||
}
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
|
||||
func (b *chartBiz) fetchVectorData(chart *model.Chart, key string, end time.Time) (cvd *VectorData, err error) {
|
||||
var (
|
||||
q string
|
||||
d *VectorData
|
||||
)
|
||||
for i, m := range chart.Metrics {
|
||||
q, err = b.formatQuery(m.Query, chart.Dashboard, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d, err = b.mb.GetVector(q, m.Legend, end); err != nil {
|
||||
log.Get("metric").Error(err)
|
||||
} else if i == 0 {
|
||||
cvd = d
|
||||
} else {
|
||||
cvd.Legend = append(cvd.Legend, d.Legend...)
|
||||
cvd.Data = append(cvd.Data, d.Data...)
|
||||
}
|
||||
}
|
||||
return cvd, nil
|
||||
}
|
||||
|
||||
func (b *chartBiz) fetchScalarData(chart *model.Chart, key string, end time.Time) (*VectorValue, error) {
|
||||
query, err := b.formatQuery(chart.Metrics[0].Query, chart.Dashboard, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := b.mb.GetScalar(query, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &VectorValue{
|
||||
//Name: "",
|
||||
Value: v,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *chartBiz) formatQuery(query, dashboard, key string) (string, error) {
|
||||
if dashboard == "home" {
|
||||
return query, nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
m := map[string]string{dashboard: key}
|
||||
q := os.Expand(query, func(k string) string {
|
||||
if v, ok := m[k]; ok {
|
||||
return v
|
||||
}
|
||||
errs = append(errs, errors.New("invalid argument in query: "+query))
|
||||
return ""
|
||||
})
|
||||
if len(errs) == 0 {
|
||||
return q, nil
|
||||
}
|
||||
return "", errs[0]
|
||||
}
|
||||
|
||||
func (b *chartBiz) getCharts(ids []string) (charts map[string]*model.Chart, err error) {
|
||||
var (
|
||||
customIds []string
|
||||
customCharts []*model.Chart
|
||||
)
|
||||
|
||||
charts = make(map[string]*model.Chart)
|
||||
for _, id := range ids {
|
||||
if id[0] == '$' {
|
||||
for _, c := range builtins {
|
||||
if c.ID == id {
|
||||
charts[id] = c
|
||||
}
|
||||
}
|
||||
} else {
|
||||
customIds = append(customIds, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(customIds) > 0 {
|
||||
if customCharts, err = b.Batch(customIds...); err == nil {
|
||||
for _, chart := range customCharts {
|
||||
charts[chart.ID] = chart
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *chartBiz) fillCharts(d *model.Dashboard) (err error) {
|
||||
if len(d.Charts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
m map[string]*model.Chart
|
||||
ids = make([]string, len(d.Charts))
|
||||
)
|
||||
|
||||
for i, c := range d.Charts {
|
||||
ids[i] = c.ID
|
||||
}
|
||||
|
||||
m, err = b.getCharts(ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range d.Charts {
|
||||
if c := m[d.Charts[i].ID]; c != nil {
|
||||
_ = copier.CopyWithOption(&d.Charts[i], c, copier.Option{IgnoreEmpty: true})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultDashboard(name, key string) *model.Dashboard {
|
||||
d := &model.Dashboard{
|
||||
Name: name,
|
||||
Key: key,
|
||||
Period: 30,
|
||||
Interval: 15,
|
||||
}
|
||||
if name == "service" {
|
||||
d.Charts = []model.ChartInfo{
|
||||
{ID: "$cpu"},
|
||||
{ID: "$memory"},
|
||||
{ID: "$network_in"},
|
||||
{ID: "$network_out"},
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
266
biz/dashboard.go
Normal file
266
biz/dashboard.go
Normal file
@ -0,0 +1,266 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/dao"
|
||||
"github.com/cuigh/swirl/model"
|
||||
"github.com/jinzhu/copier"
|
||||
)
|
||||
|
||||
var builtins = []*model.Chart{
|
||||
model.NewChart("service", "$cpu", "CPU", "${name}", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="${service}"}[5m]) * 100`, "percent:100", 60),
|
||||
model.NewChart("service", "$memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="${service}"}`, "size:bytes", 60),
|
||||
model.NewChart("service", "$network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes", 60),
|
||||
model.NewChart("service", "$network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes", 60),
|
||||
}
|
||||
|
||||
type DashboardBiz interface {
|
||||
FetchData(key string, ids []string, period time.Duration) (data.Map, error)
|
||||
FindDashboard(name, key string) (dashboard *model.Dashboard, err error)
|
||||
UpdateDashboard(dashboard *model.Dashboard, user web.User) (err error)
|
||||
}
|
||||
|
||||
func NewDashboard(d dao.Interface, mb MetricBiz, eb EventBiz) DashboardBiz {
|
||||
return &dashboardBiz{
|
||||
d: d,
|
||||
mb: mb,
|
||||
eb: eb,
|
||||
}
|
||||
}
|
||||
|
||||
type dashboardBiz struct {
|
||||
d dao.Interface
|
||||
cb ChartBiz
|
||||
mb MetricBiz
|
||||
eb EventBiz
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) FindDashboard(name, key string) (dashboard *model.Dashboard, err error) {
|
||||
if dashboard, err = b.d.DashboardGet(context.TODO(), name, key); err != nil {
|
||||
return
|
||||
}
|
||||
if dashboard == nil {
|
||||
dashboard = b.defaultDashboard(name, key)
|
||||
}
|
||||
err = b.fillCharts(dashboard)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) UpdateDashboard(dashboard *model.Dashboard, user web.User) (err error) {
|
||||
dashboard.UpdatedAt = now()
|
||||
dashboard.UpdatedBy = newOperator(user)
|
||||
return b.d.DashboardUpdate(context.TODO(), dashboard)
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) FetchData(key string, ids []string, period time.Duration) (data.Map, error) {
|
||||
if !b.mb.Enabled() {
|
||||
return data.Map{}, nil
|
||||
}
|
||||
|
||||
charts, err := b.getCharts(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
id string
|
||||
data interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
ch := make(chan Data, len(charts))
|
||||
end := time.Now()
|
||||
start := end.Add(-period)
|
||||
for _, chart := range charts {
|
||||
go func(c *model.Chart) {
|
||||
d := Data{id: c.ID}
|
||||
switch c.Type {
|
||||
case "line", "bar":
|
||||
d.data, d.err = b.fetchMatrixData(c, key, start, end)
|
||||
case "pie":
|
||||
d.data, d.err = b.fetchVectorData(c, key, end)
|
||||
case "gauge":
|
||||
d.data, d.err = b.fetchScalarData(c, key, end)
|
||||
default:
|
||||
d.err = errors.New("invalid chart type: " + c.Type)
|
||||
}
|
||||
ch <- d
|
||||
}(chart)
|
||||
}
|
||||
|
||||
ds := data.Map{}
|
||||
for range charts {
|
||||
d := <-ch
|
||||
if d.err != nil {
|
||||
log.Get("metric").Error(d.err)
|
||||
} else {
|
||||
ds.Set(d.id, d.data)
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) fetchMatrixData(chart *model.Chart, key string, start, end time.Time) (md *MatrixData, err error) {
|
||||
var (
|
||||
q string
|
||||
d *MatrixData
|
||||
)
|
||||
for i, m := range chart.Metrics {
|
||||
q, err = b.formatQuery(m.Query, chart.Dashboard, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d, err = b.mb.GetMatrix(q, m.Legend, start, end); err != nil {
|
||||
log.Get("metric").Error(err)
|
||||
} else if i == 0 {
|
||||
md = d
|
||||
} else {
|
||||
md.Legend = append(md.Legend, d.Legend...)
|
||||
md.Series = append(md.Series, d.Series...)
|
||||
}
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) fetchVectorData(chart *model.Chart, key string, end time.Time) (cvd *VectorData, err error) {
|
||||
var (
|
||||
q string
|
||||
d *VectorData
|
||||
)
|
||||
for i, m := range chart.Metrics {
|
||||
q, err = b.formatQuery(m.Query, chart.Dashboard, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d, err = b.mb.GetVector(q, m.Legend, end); err != nil {
|
||||
log.Get("metric").Error(err)
|
||||
} else if i == 0 {
|
||||
cvd = d
|
||||
} else {
|
||||
cvd.Legend = append(cvd.Legend, d.Legend...)
|
||||
cvd.Data = append(cvd.Data, d.Data...)
|
||||
}
|
||||
}
|
||||
return cvd, nil
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) fetchScalarData(chart *model.Chart, key string, end time.Time) (*VectorValue, error) {
|
||||
query, err := b.formatQuery(chart.Metrics[0].Query, chart.Dashboard, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := b.mb.GetScalar(query, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &VectorValue{
|
||||
//Name: "",
|
||||
Value: v,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) formatQuery(query, dashboard, key string) (string, error) {
|
||||
if dashboard == "home" {
|
||||
return query, nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
m := map[string]string{dashboard: key}
|
||||
q := os.Expand(query, func(k string) string {
|
||||
if v, ok := m[k]; ok {
|
||||
return v
|
||||
}
|
||||
errs = append(errs, errors.New("invalid argument in query: "+query))
|
||||
return ""
|
||||
})
|
||||
if len(errs) == 0 {
|
||||
return q, nil
|
||||
}
|
||||
return "", errs[0]
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) getCharts(ids []string) (charts map[string]*model.Chart, err error) {
|
||||
var (
|
||||
customIds []string
|
||||
customCharts []*model.Chart
|
||||
)
|
||||
|
||||
charts = make(map[string]*model.Chart)
|
||||
for _, id := range ids {
|
||||
if id[0] == '$' {
|
||||
for _, c := range builtins {
|
||||
if c.ID == id {
|
||||
charts[id] = c
|
||||
}
|
||||
}
|
||||
} else {
|
||||
customIds = append(customIds, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(customIds) > 0 {
|
||||
if customCharts, err = b.d.ChartGetBatch(context.TODO(), customIds...); err == nil {
|
||||
for _, chart := range customCharts {
|
||||
charts[chart.ID] = chart
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) fillCharts(d *model.Dashboard) (err error) {
|
||||
if len(d.Charts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
m map[string]*model.Chart
|
||||
ids = make([]string, len(d.Charts))
|
||||
)
|
||||
|
||||
for i, c := range d.Charts {
|
||||
ids[i] = c.ID
|
||||
}
|
||||
|
||||
m, err = b.getCharts(ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range d.Charts {
|
||||
if c := m[d.Charts[i].ID]; c != nil {
|
||||
_ = copier.CopyWithOption(&d.Charts[i], c, copier.Option{IgnoreEmpty: true})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *dashboardBiz) defaultDashboard(name, key string) *model.Dashboard {
|
||||
d := &model.Dashboard{
|
||||
Name: name,
|
||||
Key: key,
|
||||
Period: 30,
|
||||
Interval: 15,
|
||||
}
|
||||
if name == "service" {
|
||||
d.Charts = []model.ChartInfo{
|
||||
{ID: "$cpu"},
|
||||
{ID: "$memory"},
|
||||
{ID: "$network_in"},
|
||||
{ID: "$network_out"},
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
10
biz/node.go
10
biz/node.go
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
type NodeBiz interface {
|
||||
List() ([]*docker.Node, error)
|
||||
List(agent bool) ([]*docker.Node, error)
|
||||
Search() ([]*Node, error)
|
||||
Find(id string) (node *Node, raw string, err error)
|
||||
Delete(id, name string, user web.User) (err error)
|
||||
@ -42,15 +42,17 @@ func (b *nodeBiz) Find(id string) (node *Node, raw string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *nodeBiz) List() ([]*docker.Node, error) {
|
||||
func (b *nodeBiz) List(agent bool) ([]*docker.Node, error) {
|
||||
m, err := b.d.NodeMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes := make([]*docker.Node, 0, len(m))
|
||||
var nodes []*docker.Node
|
||||
for _, n := range m {
|
||||
nodes = append(nodes, n)
|
||||
if !agent || n.Agent != "" {
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
}
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Name < nodes[j].Name
|
||||
|
22
biz/role.go
22
biz/role.go
@ -14,6 +14,7 @@ type RoleBiz interface {
|
||||
Create(role *model.Role, user web.User) (err error)
|
||||
Delete(id, name string, user web.User) (err error)
|
||||
Update(r *model.Role, user web.User) (err error)
|
||||
GetPerms(ids []string) ([]string, error)
|
||||
}
|
||||
|
||||
func NewRole(d dao.Interface, eb EventBiz) RoleBiz {
|
||||
@ -74,3 +75,24 @@ func (b *roleBiz) Update(role *model.Role, user web.User) (err error) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *roleBiz) GetPerms(ids []string) ([]string, error) {
|
||||
m := make(map[string]struct{})
|
||||
|
||||
for _, id := range ids {
|
||||
r, err := b.d.RoleGet(context.TODO(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range r.Perms {
|
||||
m[p] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
perms := make([]string, 0, len(m))
|
||||
for p := range m {
|
||||
perms = append(perms, p)
|
||||
}
|
||||
return perms, nil
|
||||
}
|
||||
|
45
biz/session.go
Normal file
45
biz/session.go
Normal file
@ -0,0 +1,45 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/swirl/dao"
|
||||
"github.com/cuigh/swirl/model"
|
||||
)
|
||||
|
||||
type SessionBiz interface {
|
||||
Find(token string) (session *model.Session, err error)
|
||||
Create(session *model.Session) (err error)
|
||||
Update(session *model.Session) (err error)
|
||||
UpdateExpiry(id string, expiry time.Time) (err error)
|
||||
}
|
||||
|
||||
func NewSession(d dao.Interface, rb RoleBiz) SessionBiz {
|
||||
return &sessionBiz{d: d, rb: rb}
|
||||
}
|
||||
|
||||
type sessionBiz struct {
|
||||
d dao.Interface
|
||||
rb RoleBiz
|
||||
}
|
||||
|
||||
func (b *sessionBiz) Find(token string) (session *model.Session, err error) {
|
||||
return b.d.SessionGet(context.TODO(), token)
|
||||
}
|
||||
|
||||
func (b *sessionBiz) Create(session *model.Session) (err error) {
|
||||
session.CreatedAt = time.Now()
|
||||
session.UpdatedAt = session.CreatedAt
|
||||
return b.d.SessionCreate(context.TODO(), session)
|
||||
}
|
||||
|
||||
func (b *sessionBiz) Update(session *model.Session) (err error) {
|
||||
session.Dirty = false
|
||||
session.UpdatedAt = time.Now()
|
||||
return b.d.SessionUpdate(context.TODO(), session)
|
||||
}
|
||||
|
||||
func (b *sessionBiz) UpdateExpiry(id string, expiry time.Time) (err error) {
|
||||
return b.d.SessionUpdateExpiry(context.TODO(), id, expiry)
|
||||
}
|
@ -63,7 +63,9 @@ func (b *settingBiz) Save(id string, options data.Map, user web.User) (err error
|
||||
ID: id,
|
||||
Options: b.toOptions(options),
|
||||
UpdatedAt: time.Now(),
|
||||
UpdatedBy: model.Operator{ID: user.ID(), Name: user.Name()},
|
||||
}
|
||||
if user != nil {
|
||||
setting.UpdatedBy = model.Operator{ID: user.ID(), Name: user.Name()}
|
||||
}
|
||||
err = b.d.SettingUpdate(context.TODO(), setting)
|
||||
if err == nil && user != nil {
|
||||
|
18
biz/user.go
18
biz/user.go
@ -176,24 +176,6 @@ func (b *userBiz) Count() (count int, err error) {
|
||||
return b.d.UserCount(context.TODO())
|
||||
}
|
||||
|
||||
//func (b *userBiz) UpdateSession(id string) (token string, err error) {
|
||||
// session := &model.Session{
|
||||
// UserID: id,
|
||||
// Token: guid.New().String(),
|
||||
// UpdatedAt: time.Now(),
|
||||
// }
|
||||
// session.Expires = session.UpdatedAt.Add(time.Hour * 24)
|
||||
// err = b.d.SessionUpdate(context.TODO(), session)
|
||||
// if err == nil {
|
||||
// token = session.Token
|
||||
// }
|
||||
// return
|
||||
//}
|
||||
|
||||
func (b *userBiz) GetSession(token string) (session *model.Session, err error) {
|
||||
return b.d.SessionGet(context.TODO(), token)
|
||||
}
|
||||
|
||||
type UserPrivacy struct {
|
||||
ID string
|
||||
Name string
|
||||
|
57
compose.yml
57
compose.yml
@ -5,16 +5,62 @@ services:
|
||||
image: cuigh/swirl
|
||||
environment:
|
||||
DB_ADDRESS: mongodb://mongo:27017/swirl
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
DOCKER_ENDPOINT: tcp://swirl_manager_agent:2375
|
||||
AGENTS: swirl_manager_agent,swirl_worker_agent
|
||||
ports:
|
||||
- "8001:8001"
|
||||
networks:
|
||||
- net
|
||||
deploy:
|
||||
replicas: 1
|
||||
replicas: 2
|
||||
placement:
|
||||
constraints: [node.role == manager]
|
||||
constraints: [ node.role == worker ]
|
||||
|
||||
manager_agent:
|
||||
image: cuigh/socat
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
- net
|
||||
deploy:
|
||||
mode: global
|
||||
placement:
|
||||
constraints: [ node.role == manager ]
|
||||
|
||||
worker_agent:
|
||||
image: cuigh/socat
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
- net
|
||||
deploy:
|
||||
mode: global
|
||||
placement:
|
||||
constraints: [ node.role == worker ]
|
||||
|
||||
# prometheus:
|
||||
# image: prom/prometheus
|
||||
# volumes:
|
||||
# - prometheus:/prometheus
|
||||
# networks:
|
||||
# - net
|
||||
# deploy:
|
||||
# replicas: 1
|
||||
# placement:
|
||||
# constraints: [ node.labels.app.prometheus == true ]
|
||||
|
||||
# cadvisor:
|
||||
# image: gcr.io/cadvisor/cadvisor
|
||||
# volumes:
|
||||
# - /:/rootfs:ro
|
||||
# - /sys:/sys:ro
|
||||
# - /var/lib/docker:/var/lib/docker:ro
|
||||
# - /var/run:/var/run:ro
|
||||
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
# networks:
|
||||
# - net
|
||||
# deploy:
|
||||
# mode: global
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
@ -25,9 +71,10 @@ services:
|
||||
deploy:
|
||||
replicas: 1
|
||||
# placement:
|
||||
# constraints: [node.hostname == mongo]
|
||||
# constraints: [ node.labels.app.mongo == true ]
|
||||
|
||||
volumes:
|
||||
prometheus:
|
||||
mongo:
|
||||
|
||||
networks:
|
||||
|
@ -3,7 +3,7 @@ banner: false
|
||||
|
||||
web:
|
||||
entries:
|
||||
- address: :8002
|
||||
- address: :8001
|
||||
authorize: '?'
|
||||
|
||||
swirl:
|
||||
|
@ -3,10 +3,12 @@ package bolt
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/auxo/util/run"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
@ -41,6 +43,9 @@ func New(addr string) (*Dao, error) {
|
||||
logger: log.Get("bolt"),
|
||||
db: db,
|
||||
}
|
||||
run.Schedule(time.Hour, d.SessionPrune, func(err interface{}) {
|
||||
d.logger.Error("failed to clean up expired sessions: ", err)
|
||||
})
|
||||
return d, nil
|
||||
}
|
||||
|
||||
|
56
dao/bolt/session.go
Normal file
56
dao/bolt/session.go
Normal file
@ -0,0 +1,56 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/cuigh/swirl/model"
|
||||
)
|
||||
|
||||
const Session = "session"
|
||||
|
||||
func (d *Dao) SessionGet(ctx context.Context, id string) (session *model.Session, err error) {
|
||||
s := &model.Session{}
|
||||
err = d.get(Session, id, s)
|
||||
if err == ErrNoRecords {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
s = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) SessionCreate(ctx context.Context, session *model.Session) (err error) {
|
||||
return d.replace(Session, session.ID, session)
|
||||
}
|
||||
|
||||
func (d *Dao) SessionUpdate(ctx context.Context, session *model.Session) (err error) {
|
||||
return d.replace(Session, session.UserID, session)
|
||||
}
|
||||
|
||||
func (d *Dao) SessionUpdateExpiry(ctx context.Context, id string, expiry time.Time) (err error) {
|
||||
old := &model.Session{}
|
||||
return d.update(Session, id, old, func() interface{} {
|
||||
old.Expiry = expiry
|
||||
old.UpdatedAt = time.Now()
|
||||
return old
|
||||
})
|
||||
}
|
||||
|
||||
// SessionPrune cleans up expired logs.
|
||||
func (d *Dao) SessionPrune() {
|
||||
err := d.db.Update(func(tx *bolt.Tx) (err error) {
|
||||
b := tx.Bucket([]byte(Session))
|
||||
return b.ForEach(func(k, v []byte) error {
|
||||
session := &model.Session{}
|
||||
if err = decode(v, session); err == nil && session.Expiry.Add(time.Hour).Before(time.Now()) {
|
||||
err = b.Delete(k)
|
||||
}
|
||||
return err
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
d.logger.Error("failed to clean up expired sessions: ", err)
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ import (
|
||||
)
|
||||
|
||||
const User = "user"
|
||||
const Session = "session"
|
||||
|
||||
func (d *Dao) UserCount(ctx context.Context) (count int, err error) {
|
||||
return d.count(User)
|
||||
@ -124,18 +123,3 @@ func (d *Dao) UserUpdatePassword(ctx context.Context, user *model.User) (err err
|
||||
return old
|
||||
})
|
||||
}
|
||||
|
||||
func (d *Dao) SessionGet(ctx context.Context, token string) (session *model.Session, err error) {
|
||||
s := &model.Session{}
|
||||
err = d.get(Session, token, s)
|
||||
if err == ErrNoRecords {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
s = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) SessionUpdate(ctx context.Context, session *model.Session) (err error) {
|
||||
return d.replace(Session, session.UserID, session)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/app/container"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
@ -32,8 +33,10 @@ type Interface interface {
|
||||
UserUpdatePassword(ctx context.Context, user *model.User) error
|
||||
UserDelete(ctx context.Context, id string) error
|
||||
|
||||
SessionGet(ctx context.Context, token string) (*model.Session, error)
|
||||
SessionGet(ctx context.Context, id string) (*model.Session, error)
|
||||
SessionCreate(ctx context.Context, session *model.Session) error
|
||||
SessionUpdate(ctx context.Context, session *model.Session) error
|
||||
SessionUpdateExpiry(ctx context.Context, id string, expiry time.Time) (err error)
|
||||
|
||||
RegistryGet(ctx context.Context, id string) (*model.Registry, error)
|
||||
RegistryGetByURL(ctx context.Context, url string) (registry *model.Registry, err error)
|
||||
|
@ -34,12 +34,12 @@ var indexes = map[string][]mongo.IndexModel{
|
||||
mongo.IndexModel{Keys: bson.D{{"type", 1}}},
|
||||
mongo.IndexModel{Keys: bson.D{{"name", 1}}},
|
||||
},
|
||||
//"session": {
|
||||
// mongo.IndexModel{
|
||||
// Keys: bson.D{{"token", 1}},
|
||||
// Options: options.Index().SetUnique(true),
|
||||
// },
|
||||
//},
|
||||
"session": {
|
||||
mongo.IndexModel{
|
||||
Keys: bson.D{{"expiry", 1}},
|
||||
Options: options.Index().SetExpireAfterSeconds(3600),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Dao struct {
|
||||
|
38
dao/mongo/session.go
Normal file
38
dao/mongo/session.go
Normal file
@ -0,0 +1,38 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/swirl/model"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
const Session = "session"
|
||||
|
||||
func (d *Dao) SessionGet(ctx context.Context, id string) (session *model.Session, err error) {
|
||||
session = &model.Session{}
|
||||
found, err := d.find(ctx, Session, id, session)
|
||||
if !found {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) SessionCreate(ctx context.Context, session *model.Session) (err error) {
|
||||
return d.create(ctx, Session, session)
|
||||
}
|
||||
|
||||
func (d *Dao) SessionUpdate(ctx context.Context, session *model.Session) (err error) {
|
||||
return d.update(ctx, Session, session.ID, session)
|
||||
}
|
||||
|
||||
func (d *Dao) SessionUpdateExpiry(ctx context.Context, id string, expiry time.Time) (err error) {
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"expiry": expiry,
|
||||
"updated_by": time.Now(),
|
||||
},
|
||||
}
|
||||
return d.update(ctx, Session, id, update)
|
||||
}
|
@ -6,13 +6,9 @@ import (
|
||||
"github.com/cuigh/swirl/model"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
User = "user"
|
||||
Session = "session"
|
||||
)
|
||||
const User = "user"
|
||||
|
||||
func (d *Dao) UserCount(ctx context.Context) (int, error) {
|
||||
count, err := d.db.Collection(User).CountDocuments(ctx, bson.M{})
|
||||
@ -118,19 +114,3 @@ func (d *Dao) UserUpdatePassword(ctx context.Context, user *model.User) (err err
|
||||
}
|
||||
return d.update(ctx, User, user.ID, update)
|
||||
}
|
||||
|
||||
func (d *Dao) SessionGet(ctx context.Context, token string) (session *model.Session, err error) {
|
||||
session = &model.Session{}
|
||||
err = d.db.Collection(Session).FindOne(ctx, bson.M{"token": token}).Decode(session)
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dao) SessionUpdate(ctx context.Context, session *model.Session) (err error) {
|
||||
_, err = d.db.Collection(Session).UpdateByID(ctx, session.UserID, session, options.Update().SetUpsert(true))
|
||||
return
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@ -12,7 +12,6 @@ require (
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect
|
||||
github.com/gobwas/pool v0.2.0 // indirect
|
||||
github.com/gobwas/ws v1.0.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/jinzhu/copier v0.3.2
|
||||
github.com/mattn/go-shellwords v1.0.3
|
||||
|
2
go.sum
2
go.sum
@ -344,8 +344,6 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
|
2
main.go
2
main.go
@ -30,7 +30,7 @@ var (
|
||||
|
||||
func main() {
|
||||
app.Name = "Swirl"
|
||||
app.Version = "1.0.0beta5"
|
||||
app.Version = "1.0.0beta6"
|
||||
app.Desc = "A web management UI for Docker, focused on swarm cluster"
|
||||
app.Action = func(ctx *app.Context) error {
|
||||
return run.Pipeline(misc.LoadOptions, initSystem, scaler.Start, startServer)
|
||||
|
@ -253,10 +253,14 @@ func (cd *Dashboard) ID() string {
|
||||
type Session struct {
|
||||
ID string `json:"id" bson:"_id"` // token
|
||||
UserID string `json:"userId" bson:"user_id"`
|
||||
Username string `json:"username" bson:"username"`
|
||||
Admin bool `json:"admin" bson:"admin"`
|
||||
Roles []string `json:"roles" bson:"roles"`
|
||||
Perm int64 `json:"perm" bson:"perm"`
|
||||
Perm uint64 `json:"perm" bson:"perm"`
|
||||
Perms []string `json:"-" bson:"-"`
|
||||
Dirty bool `json:"dirty" bson:"dirty"`
|
||||
Expiry time.Time `json:"expiry" bson:"expiry"`
|
||||
MaxExpiry time.Time `json:"maxExpiry" bson:"max_expiry"`
|
||||
CreatedAt time.Time `json:"createdAt" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updated_at"`
|
||||
}
|
||||
|
306
security/auth.go
Normal file
306
security/auth.go
Normal file
@ -0,0 +1,306 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/auxo/security"
|
||||
"github.com/cuigh/auxo/security/certify"
|
||||
"github.com/cuigh/auxo/security/certify/ldap"
|
||||
"github.com/cuigh/auxo/security/passwd"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/cuigh/swirl/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// Identifier identifies the user.
|
||||
type Identifier struct {
|
||||
ub biz.UserBiz
|
||||
rb biz.RoleBiz
|
||||
sb biz.SessionBiz
|
||||
realms []RealmFunc
|
||||
extractors []TokenExtractor
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewIdentifier(s *misc.Setting, ub biz.UserBiz, rb biz.RoleBiz, sb biz.SessionBiz) *Identifier {
|
||||
return &Identifier{
|
||||
ub: ub,
|
||||
rb: rb,
|
||||
sb: sb,
|
||||
realms: []RealmFunc{internalRealm(), ldapRealm(s, ub)},
|
||||
extractors: []TokenExtractor{headerExtractor, queryExtractor},
|
||||
logger: log.Get(PkgName),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Identifier) Apply(next web.HandlerFunc) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
token := c.extractToken(ctx)
|
||||
if token != "" {
|
||||
user := c.identifyUser(token)
|
||||
ctx.SetUser(user)
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Identifier) Identify(loginName, password string) (identify Identity, err error) {
|
||||
var (
|
||||
u security.User
|
||||
s *model.Session
|
||||
)
|
||||
|
||||
u, err = c.signIn(loginName, password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s, err = c.createSession(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UserInfo{
|
||||
id: u.ID(),
|
||||
name: u.Name(),
|
||||
token: s.ID,
|
||||
perms: s.Perms,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Identifier) signIn(loginName, password string) (user security.User, err error) {
|
||||
privacy, err := c.ub.FindPrivacy(loginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if privacy != nil && privacy.Status == biz.UserStatusBlocked {
|
||||
return nil, misc.Error(misc.ErrAccountDisabled, certify.ErrAccountDisabled)
|
||||
}
|
||||
|
||||
for _, login := range c.realms {
|
||||
user, err = login(privacy, loginName, password)
|
||||
if user != nil && err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = misc.Error(misc.ErrInvalidToken, certify.ErrInvalidToken)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Identifier) extractToken(ctx web.Context) (token string) {
|
||||
for _, e := range c.extractors {
|
||||
if token = e(ctx); token != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Identifier) identifyUser(token string) web.User {
|
||||
session, err := c.sb.Find(token)
|
||||
if err != nil {
|
||||
c.logger.Error("failed to find session: ", err)
|
||||
return nil
|
||||
} else if session == nil || session.Expiry.Before(time.Now()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if session.Dirty {
|
||||
if err = c.updateSession(session); err != nil {
|
||||
c.logger.Error("failed to refresh session: ", err)
|
||||
return nil
|
||||
}
|
||||
} else if time.Now().Add(time.Minute * 5).After(session.Expiry) {
|
||||
c.renewSession(session)
|
||||
}
|
||||
|
||||
return c.createUser(session)
|
||||
}
|
||||
|
||||
func (c *Identifier) createUser(s *model.Session) web.User {
|
||||
return &User{
|
||||
token: s.ID,
|
||||
id: s.UserID,
|
||||
name: s.Username,
|
||||
admin: s.Admin,
|
||||
perm: PermMap(s.Perm),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Identifier) createSession(user security.User) (s *model.Session, err error) {
|
||||
s = &model.Session{
|
||||
ID: primitive.NewObjectID().Hex(),
|
||||
UserID: user.ID(),
|
||||
Username: user.Name(),
|
||||
Expiry: time.Now().Add(misc.Options.TokenExpiry),
|
||||
}
|
||||
s.MaxExpiry = s.Expiry.Add(24 * time.Hour)
|
||||
if err = c.fillSession(s); err == nil {
|
||||
err = c.sb.Create(s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Identifier) updateSession(s *model.Session) (err error) {
|
||||
if err = c.fillSession(s); err == nil {
|
||||
err = c.sb.Update(s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Identifier) fillSession(s *model.Session) (err error) {
|
||||
u, err := c.ub.FindByID(s.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if u == nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
if u.Admin {
|
||||
s.Perms = []string{"*"}
|
||||
} else {
|
||||
s.Perms, err = c.rb.GetPerms(u.Roles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Perm = uint64(NewPermMap(s.Perms))
|
||||
}
|
||||
|
||||
s.Username = u.Name
|
||||
s.Admin = u.Admin
|
||||
s.Roles = u.Roles
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Identifier) renewSession(s *model.Session) {
|
||||
expiry := time.Now().Add(misc.Options.TokenExpiry)
|
||||
if expiry.After(s.MaxExpiry) {
|
||||
expiry = s.MaxExpiry
|
||||
}
|
||||
err := c.sb.UpdateExpiry(s.ID, expiry)
|
||||
if err != nil {
|
||||
c.logger.Errorf("failed to renew token '%s': %s", s.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
type TokenExtractor func(ctx web.Context) string
|
||||
|
||||
func headerExtractor(ctx web.Context) (token string) {
|
||||
const prefix = "Bearer "
|
||||
if value := ctx.Header(web.HeaderAuthorization); strings.HasPrefix(value, prefix) {
|
||||
token = value[len(prefix):]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func queryExtractor(ctx web.Context) (token string) {
|
||||
return ctx.Query("token")
|
||||
}
|
||||
|
||||
type RealmFunc func(u *biz.UserPrivacy, loginName, password string) (security.User, error)
|
||||
|
||||
func internalRealm() RealmFunc {
|
||||
return func(u *biz.UserPrivacy, loginName, password string) (security.User, error) {
|
||||
if u == nil || u.Type != biz.UserTypeInternal {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if passwd.Validate(password, u.Password, u.Salt) {
|
||||
return security.NewUser(u.ID, u.Name), nil
|
||||
}
|
||||
return nil, misc.Error(misc.ErrInvalidToken, certify.ErrInvalidToken)
|
||||
}
|
||||
}
|
||||
|
||||
func ldapRealm(s *misc.Setting, ub biz.UserBiz) RealmFunc {
|
||||
const authBind = "bind"
|
||||
var r certify.Realm
|
||||
|
||||
if s.LDAP.Enabled {
|
||||
opts := []ldap.Option{
|
||||
ldap.NameAttr(s.LDAP.NameAttr),
|
||||
ldap.EmailAttr(s.LDAP.EmailAttr),
|
||||
ldap.UserFilter(s.LDAP.UserFilter),
|
||||
ldap.Security(ldap.SecurityPolicy(s.LDAP.Security)),
|
||||
}
|
||||
if s.LDAP.Authentication == authBind {
|
||||
opts = append(opts, ldap.Binding(s.LDAP.BindDN, s.LDAP.BindPassword))
|
||||
}
|
||||
r = ldap.New(s.LDAP.Address, s.LDAP.BaseDN, s.LDAP.UserDN, opts...)
|
||||
}
|
||||
|
||||
return func(u *biz.UserPrivacy, loginName, password string) (security.User, error) {
|
||||
if r == nil || (u != nil && u.Type != biz.UserTypeLDAP) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
user, err := r.Login(certify.NewSimpleToken(loginName, password))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
id string
|
||||
lu = user.(*ldap.User)
|
||||
)
|
||||
if u == nil {
|
||||
id, err = ub.Create(&model.User{
|
||||
Type: biz.UserTypeLDAP,
|
||||
LoginName: loginName,
|
||||
Name: lu.Name(),
|
||||
Email: lu.Email(),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lu.SetID(id)
|
||||
} else {
|
||||
lu.SetID(u.ID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
type Identity interface {
|
||||
ID() string
|
||||
Name() string
|
||||
Anonymous() bool
|
||||
Token() string
|
||||
Perms() []string
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
id string
|
||||
name string
|
||||
token string
|
||||
perms []string
|
||||
}
|
||||
|
||||
func (u *UserInfo) ID() string {
|
||||
return u.id
|
||||
}
|
||||
|
||||
func (u *UserInfo) Name() string {
|
||||
return u.name
|
||||
}
|
||||
|
||||
func (u *UserInfo) Anonymous() bool {
|
||||
return u.id == ""
|
||||
}
|
||||
|
||||
func (u *UserInfo) Token() string {
|
||||
return u.token
|
||||
}
|
||||
|
||||
func (u *UserInfo) Perms() []string {
|
||||
return u.perms
|
||||
}
|
151
security/jwt.go
151
security/jwt.go
@ -1,151 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/auxo/security"
|
||||
"github.com/cuigh/auxo/util/cast"
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var ErrNoNeedRefresh = errors.New("no need to refresh")
|
||||
|
||||
type JWT struct {
|
||||
Schema string
|
||||
Sources data.Options
|
||||
KeyFunc jwt.Keyfunc
|
||||
Identifier func(token *jwt.Token) web.User
|
||||
tokenExpiry int64
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewIdentifier() web.Filter {
|
||||
logger := log.Get("security")
|
||||
key := misc.Options.TokenKey
|
||||
expiry := misc.Options.TokenExpiry
|
||||
if key == "" {
|
||||
key = "swirl"
|
||||
logger.Warnf("Swirl is using default token key as token_key isn't configured, this may cause security problems")
|
||||
}
|
||||
if expiry == 0 {
|
||||
expiry = 30 * time.Minute
|
||||
}
|
||||
|
||||
return &JWT{
|
||||
logger: logger,
|
||||
tokenExpiry: int64(expiry.Seconds()),
|
||||
Schema: "Bearer",
|
||||
Sources: data.Options{
|
||||
{Name: "header", Value: web.HeaderAuthorization},
|
||||
},
|
||||
KeyFunc: func(token *jwt.Token) (interface{}, error) {
|
||||
// TODO: use user salt as key?
|
||||
return []byte(key), nil
|
||||
},
|
||||
Identifier: func(token *jwt.Token) web.User {
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
return security.NewUser(cast.ToString(claims["sub"]), cast.ToString(claims["name"]))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JWT) Apply(next web.HandlerFunc) web.HandlerFunc {
|
||||
if j.KeyFunc == nil {
|
||||
panic("KeyFunc is required")
|
||||
}
|
||||
if j.Schema == "" {
|
||||
j.Schema = "Bearer"
|
||||
}
|
||||
if len(j.Sources) == 0 {
|
||||
j.Sources = data.Options{
|
||||
{Name: "header", Value: web.HeaderAuthorization},
|
||||
}
|
||||
}
|
||||
if j.Identifier == nil {
|
||||
j.Identifier = func(token *jwt.Token) web.User {
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
return security.NewUser(cast.ToString(claims["sub"]), cast.ToString(claims["name"]))
|
||||
}
|
||||
}
|
||||
|
||||
return func(ctx web.Context) error {
|
||||
ts := j.extractToken(ctx)
|
||||
if ts != "" {
|
||||
token, err := jwt.Parse(ts, j.KeyFunc)
|
||||
if err != nil {
|
||||
j.logger.Debugf("failed to parse token: %s", err)
|
||||
} else {
|
||||
user := j.Identifier(token)
|
||||
ctx.SetUser(user)
|
||||
if ts, err = j.refreshToken(user, token); err == nil {
|
||||
ctx.SetHeader(web.HeaderAuthorization, ts)
|
||||
} else if err != ErrNoNeedRefresh {
|
||||
j.logger.Errorf("failed to refresh token: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JWT) extractToken(ctx web.Context) (token string) {
|
||||
for _, src := range j.Sources {
|
||||
switch src.Name {
|
||||
case "header":
|
||||
token = ctx.Header(src.Value)
|
||||
if strings.HasPrefix(token, j.Schema) {
|
||||
return token[len(j.Schema)+1:]
|
||||
}
|
||||
case "cookie":
|
||||
if cookie, err := ctx.Cookie(src.Value); err == nil {
|
||||
token = cookie.Value
|
||||
}
|
||||
case "form":
|
||||
token = ctx.Form(src.Value)
|
||||
case "query":
|
||||
token = ctx.Query(src.Value)
|
||||
}
|
||||
if token != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (j *JWT) refreshToken(user web.User, token *jwt.Token) (string, error) {
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
expiry := cast.ToInt64(claims["exp"])
|
||||
now := time.Now().Unix()
|
||||
// refresh token when remaining expiry is less than 5 minutes
|
||||
if (expiry - now) < 5*60 {
|
||||
ts, err := j.CreateToken(user.ID(), user.Name())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ts, nil
|
||||
}
|
||||
return "", ErrNoNeedRefresh
|
||||
}
|
||||
|
||||
func (j *JWT) CreateToken(id, name string) (string, error) {
|
||||
now := time.Now().Unix()
|
||||
claims := jwt.MapClaims{
|
||||
"name": name,
|
||||
"sub": id,
|
||||
"iat": now,
|
||||
"exp": now + j.tokenExpiry,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
key, err := j.KeyFunc(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token.SignedString(key)
|
||||
}
|
171
security/perm.go
171
security/perm.go
@ -2,34 +2,79 @@ package security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/cuigh/auxo/cache"
|
||||
"github.com/cuigh/auxo/log"
|
||||
"github.com/cuigh/auxo/net/web"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
)
|
||||
|
||||
type Authorizer struct {
|
||||
ub biz.UserBiz
|
||||
perms *cache.Value
|
||||
logger log.Logger
|
||||
const ActionBits = 24
|
||||
|
||||
// Resources holds all resources requiring authorization. Up to 40 resources are supported.
|
||||
// WARN: DO NOT CHANGE VALUES!!!
|
||||
var Resources = map[string]uint64{
|
||||
"registry": 1,
|
||||
"node": 1 << 1,
|
||||
"network": 1 << 2,
|
||||
"service": 1 << 3,
|
||||
"task": 1 << 4,
|
||||
"stack": 1 << 5,
|
||||
"config": 1 << 6,
|
||||
"secret": 1 << 7,
|
||||
"image": 1 << 8,
|
||||
"container": 1 << 9,
|
||||
"volume": 1 << 10,
|
||||
"user": 1 << 11,
|
||||
"role": 1 << 12,
|
||||
"chart": 1 << 13,
|
||||
"dashboard": 1 << 14,
|
||||
"event": 1 << 15,
|
||||
"setting": 1 << 16,
|
||||
}
|
||||
|
||||
func NewAuthorizer(ub biz.UserBiz, rb biz.RoleBiz) web.Filter {
|
||||
v := cache.Value{
|
||||
TTL: 5 * time.Minute,
|
||||
Load: func() (interface{}, error) { return loadPerms(rb) },
|
||||
}
|
||||
return &Authorizer{
|
||||
ub: ub,
|
||||
perms: &v,
|
||||
logger: log.Get("security"),
|
||||
}
|
||||
// Actions holds all actions requiring authorization. Up to 24 actions are supported.
|
||||
// WARN: DO NOT CHANGE VALUES!!!
|
||||
var Actions = map[string]uint64{
|
||||
"view": 1,
|
||||
"edit": 1 << 1,
|
||||
"delete": 1 << 2,
|
||||
"disconnect": 1 << 3,
|
||||
"restart": 1 << 4,
|
||||
"rollback": 1 << 5,
|
||||
"logs": 1 << 6,
|
||||
"deploy": 1 << 7,
|
||||
"shutdown": 1 << 8,
|
||||
"execute": 1 << 9,
|
||||
}
|
||||
|
||||
var Perms = map[string][]string{
|
||||
"registry": {"view", "edit", "delete"},
|
||||
"node": {"view", "edit", "delete"},
|
||||
"network": {"view", "edit", "delete", "disconnect"},
|
||||
"service": {"view", "edit", "delete", "restart", "rollback", "logs"},
|
||||
"task": {"view", "logs"},
|
||||
"stack": {"view", "edit", "delete", "deploy", "shutdown"},
|
||||
"config": {"view", "edit", "delete"},
|
||||
"secret": {"view", "edit", "delete"},
|
||||
"image": {"view", "delete"},
|
||||
"container": {"view", "delete", "logs", "execute"},
|
||||
"volume": {"view", "edit", "delete"},
|
||||
"user": {"view", "edit", "delete"},
|
||||
"role": {"view", "edit", "delete"},
|
||||
"chart": {"view", "edit", "delete"},
|
||||
"dashboard": {"edit"},
|
||||
"event": {"view"},
|
||||
"setting": {"view", "edit"},
|
||||
}
|
||||
|
||||
type Authorizer struct {
|
||||
}
|
||||
|
||||
func NewAuthorizer() Authorizer {
|
||||
return Authorizer{}
|
||||
}
|
||||
|
||||
// Apply implements `web.Filter` interface.
|
||||
func (a *Authorizer) Apply(next web.HandlerFunc) web.HandlerFunc {
|
||||
func (p Authorizer) Apply(next web.HandlerFunc) web.HandlerFunc {
|
||||
return func(ctx web.Context) error {
|
||||
auth := ctx.Handler().Authorize()
|
||||
|
||||
@ -43,65 +88,65 @@ func (a *Authorizer) Apply(next web.HandlerFunc) web.HandlerFunc {
|
||||
return web.NewError(http.StatusUnauthorized, "You are not logged in")
|
||||
}
|
||||
|
||||
if auth != web.AuthAuthenticated && !a.check(user, auth) {
|
||||
if auth != web.AuthAuthenticated && !p.check(user, auth) {
|
||||
return web.NewError(http.StatusForbidden, "You do not have access to this resource")
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Authorizer) check(user web.User, auth string) bool {
|
||||
u, err := a.ub.FindByID(user.ID())
|
||||
if err != nil {
|
||||
a.logger.Errorf("failed to query user '%s': %s", user.ID(), err)
|
||||
return false
|
||||
}
|
||||
|
||||
if u == nil || u.Status == biz.UserStatusBlocked {
|
||||
return false
|
||||
} else if u.Admin {
|
||||
func (p Authorizer) check(user web.User, auth string) bool {
|
||||
u := user.(*User)
|
||||
if u.admin {
|
||||
return true
|
||||
} else if auth == web.AuthAdministrator {
|
||||
return u.Admin
|
||||
}
|
||||
return u.perm.Contains(auth)
|
||||
}
|
||||
|
||||
v, err := a.perms.Get(true)
|
||||
if err != nil {
|
||||
a.logger.Error("failed to load role perms: ", err)
|
||||
return false
|
||||
}
|
||||
type PermMap uint64
|
||||
|
||||
perms := v.(map[string]PermSet)
|
||||
for _, r := range u.Roles {
|
||||
if set, ok := perms[r]; ok {
|
||||
if set.Contains(auth) {
|
||||
return true
|
||||
}
|
||||
func NewPermMap(perms []string) PermMap {
|
||||
var p uint64
|
||||
for _, perm := range perms {
|
||||
pair := strings.SplitN(perm, ".", 2)
|
||||
if len(pair) == 2 {
|
||||
r, a := Resources[pair[0]], Actions[pair[1]]
|
||||
p |= r << ActionBits
|
||||
p |= a
|
||||
}
|
||||
}
|
||||
return PermMap(p)
|
||||
}
|
||||
|
||||
func (p PermMap) Contains(perm string) bool {
|
||||
pair := strings.SplitN(perm, ".", 2)
|
||||
if len(pair) == 2 {
|
||||
r, a := Resources[pair[0]], Actions[pair[1]]
|
||||
return uint64(p)&(r<<ActionBits) > 0 && uint64(p)&a > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loadPerms(rb biz.RoleBiz) (interface{}, error) {
|
||||
roles, err := rb.Search("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
perms := make(map[string]PermSet)
|
||||
for _, role := range roles {
|
||||
set := make(PermSet)
|
||||
for _, p := range role.Perms {
|
||||
set[p] = struct{}{}
|
||||
}
|
||||
perms[role.ID] = set
|
||||
}
|
||||
return perms, nil
|
||||
type User struct {
|
||||
token string
|
||||
id string
|
||||
name string
|
||||
admin bool
|
||||
perm PermMap
|
||||
}
|
||||
|
||||
type PermSet map[string]struct{}
|
||||
|
||||
func (s PermSet) Contains(perm string) (ok bool) {
|
||||
_, ok = s[perm]
|
||||
return
|
||||
func (u *User) Token() string {
|
||||
return u.token
|
||||
}
|
||||
|
||||
func (u *User) ID() string {
|
||||
return u.id
|
||||
}
|
||||
|
||||
func (u *User) Name() string {
|
||||
return u.name
|
||||
}
|
||||
|
||||
func (u *User) Anonymous() bool {
|
||||
return u.id == ""
|
||||
}
|
||||
|
@ -2,116 +2,11 @@ package security
|
||||
|
||||
import (
|
||||
"github.com/cuigh/auxo/app/container"
|
||||
"github.com/cuigh/auxo/security"
|
||||
"github.com/cuigh/auxo/security/certify"
|
||||
"github.com/cuigh/auxo/security/certify/ldap"
|
||||
"github.com/cuigh/auxo/security/passwd"
|
||||
"github.com/cuigh/swirl/biz"
|
||||
"github.com/cuigh/swirl/misc"
|
||||
"github.com/cuigh/swirl/model"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
ub biz.UserBiz
|
||||
realms []RealmFunc
|
||||
}
|
||||
|
||||
func NewAuthenticator(s *misc.Setting, ub biz.UserBiz) *Authenticator {
|
||||
return &Authenticator{
|
||||
ub: ub,
|
||||
realms: []RealmFunc{internalRealm(), ldapRealm(s, ub)},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Authenticator) Login(loginName, password string) (user security.User, err error) {
|
||||
privacy, err := a.ub.FindPrivacy(loginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if privacy != nil && privacy.Status == biz.UserStatusBlocked {
|
||||
return nil, misc.Error(misc.ErrAccountDisabled, certify.ErrAccountDisabled)
|
||||
}
|
||||
|
||||
for _, login := range a.realms {
|
||||
user, err = login(privacy, loginName, password)
|
||||
if user != nil && err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = misc.Error(misc.ErrInvalidToken, certify.ErrInvalidToken)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type RealmFunc func(u *biz.UserPrivacy, loginName, password string) (security.User, error)
|
||||
|
||||
func internalRealm() RealmFunc {
|
||||
return func(u *biz.UserPrivacy, loginName, password string) (security.User, error) {
|
||||
if u == nil || u.Type != biz.UserTypeInternal {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if passwd.Validate(password, u.Password, u.Salt) {
|
||||
return security.NewUser(u.ID, u.Name), nil
|
||||
}
|
||||
return nil, misc.Error(misc.ErrInvalidToken, certify.ErrInvalidToken)
|
||||
}
|
||||
}
|
||||
|
||||
func ldapRealm(s *misc.Setting, ub biz.UserBiz) RealmFunc {
|
||||
const authBind = "bind"
|
||||
var r certify.Realm
|
||||
|
||||
if s.LDAP.Enabled {
|
||||
opts := []ldap.Option{
|
||||
ldap.NameAttr(s.LDAP.NameAttr),
|
||||
ldap.EmailAttr(s.LDAP.EmailAttr),
|
||||
ldap.UserFilter(s.LDAP.UserFilter),
|
||||
ldap.Security(ldap.SecurityPolicy(s.LDAP.Security)),
|
||||
}
|
||||
if s.LDAP.Authentication == authBind {
|
||||
opts = append(opts, ldap.Binding(s.LDAP.BindDN, s.LDAP.BindPassword))
|
||||
}
|
||||
r = ldap.New(s.LDAP.Address, s.LDAP.BaseDN, s.LDAP.UserDN, opts...)
|
||||
}
|
||||
|
||||
return func(u *biz.UserPrivacy, loginName, password string) (security.User, error) {
|
||||
if r == nil || (u != nil && u.Type != biz.UserTypeLDAP) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
user, err := r.Login(certify.NewSimpleToken(loginName, password))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
id string
|
||||
lu = user.(*ldap.User)
|
||||
)
|
||||
if u == nil {
|
||||
id, err = ub.Create(&model.User{
|
||||
Type: biz.UserTypeLDAP,
|
||||
LoginName: loginName,
|
||||
Name: lu.Name(),
|
||||
Email: lu.Email(),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lu.SetID(id)
|
||||
} else {
|
||||
lu.SetID(u.ID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
const PkgName = "security"
|
||||
|
||||
func init() {
|
||||
container.Put(NewAuthenticator)
|
||||
container.Put(NewIdentifier, container.Name("identifier"))
|
||||
container.Put(NewAuthorizer, container.Name("authorizer"))
|
||||
}
|
||||
|
@ -25,23 +25,19 @@ class Ajax {
|
||||
|
||||
this.ajax.interceptors.request.use(
|
||||
(config: any) => {
|
||||
if (store.state.token) {
|
||||
config.headers.Authorization = "Bearer " + store.state.token
|
||||
if (store.state.user?.token) {
|
||||
config.headers.Authorization = "Bearer " + store.state.user.token
|
||||
}
|
||||
// store.commit(Mutations.SetAjaxLoading, true);
|
||||
return config;
|
||||
},
|
||||
(error: any) => {
|
||||
console.error(error); // for debug
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
|
||||
this.ajax.interceptors.response.use(
|
||||
(response: any) => {
|
||||
if (response.headers.authorization) {
|
||||
store.commit(Mutations.SetToken, response.headers.authorization)
|
||||
}
|
||||
// store.commit(Mutations.SetAjaxLoading, false);
|
||||
return response;
|
||||
},
|
||||
|
@ -32,29 +32,6 @@ export interface Chart {
|
||||
};
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
name: string;
|
||||
key: string;
|
||||
period: number;
|
||||
interval: number;
|
||||
charts: ChartInfo[];
|
||||
}
|
||||
|
||||
export interface ChartInfo {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'line' | 'bar' | 'pie' | 'gauge';
|
||||
unit: string;
|
||||
width: number;
|
||||
height: number;
|
||||
margin: {
|
||||
left?: number;
|
||||
right?: number;
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SearchArgs {
|
||||
name?: string;
|
||||
dashboard?: string;
|
||||
@ -83,18 +60,6 @@ export class ChartApi {
|
||||
delete(id: string, title: string) {
|
||||
return ajax.post<Result<Object>>('/chart/delete', { id, title })
|
||||
}
|
||||
|
||||
fetchData(key: string, charts: string[], period: number) {
|
||||
return ajax.get<any>('/chart/fetch-data', { key, charts: charts.join(","), period })
|
||||
}
|
||||
|
||||
findDashboard(name: string, key: string) {
|
||||
return ajax.get<Dashboard>('/chart/find-dashboard', { name, key })
|
||||
}
|
||||
|
||||
saveDashboard(dashboard: Dashboard) {
|
||||
return ajax.post<Result<Object>>('/chart/save-dashboard', dashboard)
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChartApi
|
||||
|
40
ui/src/api/dashboard.ts
Normal file
40
ui/src/api/dashboard.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import ajax, { Result } from './ajax'
|
||||
|
||||
export interface Dashboard {
|
||||
name: string;
|
||||
key: string;
|
||||
period: number;
|
||||
interval: number;
|
||||
charts: ChartInfo[];
|
||||
}
|
||||
|
||||
export interface ChartInfo {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'line' | 'bar' | 'pie' | 'gauge';
|
||||
unit: string;
|
||||
width: number;
|
||||
height: number;
|
||||
margin: {
|
||||
left?: number;
|
||||
right?: number;
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class DashboardApi {
|
||||
fetchData(key: string, charts: string[], period: number) {
|
||||
return ajax.get<any>('/dashboard/fetch-data', { key, charts: charts.join(","), period })
|
||||
}
|
||||
|
||||
find(name: string, key: string) {
|
||||
return ajax.get<Dashboard>('/dashboard/find', { name, key })
|
||||
}
|
||||
|
||||
save(dashboard: Dashboard) {
|
||||
return ajax.post<Result<Object>>('/dashboard/save', dashboard)
|
||||
}
|
||||
}
|
||||
|
||||
export default new DashboardApi
|
@ -2,8 +2,8 @@ import ajax, { Result } from './ajax'
|
||||
|
||||
export interface AuthUser {
|
||||
token: string;
|
||||
id: string;
|
||||
name: string;
|
||||
perms: string[];
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
@ -89,8 +89,10 @@ import {
|
||||
import { XChart } from "@/components/chart";
|
||||
import dragula from 'dragula'
|
||||
import 'dragula/dist/dragula.css'
|
||||
import chartApi, { ChartInfo } from "@/api/chart";
|
||||
import type { Chart, Dashboard } from "@/api/chart";
|
||||
import dashboardApi from "@/api/dashboard";
|
||||
import type { Dashboard, ChartInfo } from "@/api/dashboard";
|
||||
import chartApi from "@/api/chart";
|
||||
import type { Chart } from "@/api/chart";
|
||||
import { isEmpty, useTimer } from "@/utils";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@ -170,7 +172,7 @@ const setChartRefs = (c: any) => c && charts.set(c.id, c)
|
||||
var stop: () => void
|
||||
|
||||
async function saveDashboard() {
|
||||
await chartApi.saveDashboard(dashboard.value);
|
||||
await dashboardApi.save(dashboard.value);
|
||||
window.message.success(t('texts.action_success'))
|
||||
}
|
||||
|
||||
@ -219,7 +221,7 @@ function removeChart(id: string) {
|
||||
async function refreshData() {
|
||||
if (dashboard.value != null && !isEmpty(dashboard.value.charts)) {
|
||||
const ids = dashboard.value.charts.map(i => i.id)
|
||||
const r = await chartApi.fetchData(props.name, ids, dashboard.value.period || 30);
|
||||
const r = await dashboardApi.fetchData(props.name, ids, dashboard.value.period || 30);
|
||||
charts.forEach((v, k) => {
|
||||
r.data[k] && v.setData(r.data[k])
|
||||
})
|
||||
@ -258,7 +260,7 @@ function sortCharts() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
chartApi.findDashboard(props.type, props.name).then(r => {
|
||||
dashboardApi.find(props.type, props.name).then(r => {
|
||||
dashboard.value = r.data as Dashboard
|
||||
initDrag()
|
||||
stop = useTimer(refreshData, (dashboard.value.interval || 10) * 1000)
|
||||
|
@ -64,7 +64,7 @@
|
||||
<PersonOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ store.state.name }}
|
||||
{{ store.state.user?.name }}
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
<n-tooltip trigger="hover">
|
||||
|
@ -40,7 +40,7 @@ export default {
|
||||
"logs": "View logs",
|
||||
"deploy": "Deploy",
|
||||
"shutdown": "Shutdown",
|
||||
"dashboard": "Dashboard",
|
||||
"execute": "Execute",
|
||||
},
|
||||
"fields": {
|
||||
"home": "Home",
|
||||
@ -253,6 +253,7 @@ export default {
|
||||
"user": "User | Users",
|
||||
"role": "Role | Roles",
|
||||
"chart": "Chart | Charts",
|
||||
"dashboard": "Dashboard | Dashboards",
|
||||
"event": "Event | Events",
|
||||
"setting": "Setting | Settings",
|
||||
},
|
||||
|
@ -40,7 +40,7 @@ export default {
|
||||
"logs": "查看日志",
|
||||
"deploy": "发布",
|
||||
"shutdown": "下线",
|
||||
"dashboard": "仪表盘",
|
||||
"execute": "执行",
|
||||
},
|
||||
"fields": {
|
||||
"home": "首页",
|
||||
@ -253,6 +253,7 @@ export default {
|
||||
"user": "用户",
|
||||
"role": "角色",
|
||||
"chart": "图表",
|
||||
"dashboard": "仪表盘",
|
||||
"event": "事件",
|
||||
"setting": "设置",
|
||||
},
|
||||
|
@ -74,7 +74,7 @@ const rules = {
|
||||
password: requiredRule(),
|
||||
};
|
||||
const { submit, submiting } = useForm<AuthUser>(form, () => userApi.login(model), (user: AuthUser) => {
|
||||
store.commit(Mutations.Login, user);
|
||||
store.commit(Mutations.SetUser, user);
|
||||
let redirect = decodeURIComponent(<string>route.query.redirect || "/");
|
||||
router.push({ path: redirect });
|
||||
})
|
||||
|
@ -52,10 +52,12 @@
|
||||
<x-code :code="raw" language="json" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="logs" :tab="t('fields.logs')" display-directive="show:lazy">
|
||||
<x-logs type="container" :node="node" :id="model.id"></x-logs>
|
||||
<x-logs type="container" :node="node" :id="model.id" v-if="store.getters.allow('container.logs')"></x-logs>
|
||||
<n-alert type="info" v-else>{{ t('texts.403') }}</n-alert>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="exec" :tab="t('fields.execute')" display-directive="show:lazy">
|
||||
<execute :node="node" :id="model.id"></execute>
|
||||
<execute :node="node" :id="model.id" v-if="store.getters.allow('container.execute')"></execute>
|
||||
<n-alert type="info" v-else>{{ t('texts.403') }}</n-alert>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
@ -71,8 +73,10 @@ import {
|
||||
NTable,
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NAlert,
|
||||
} from "naive-ui";
|
||||
import { ArrowBackCircleOutline as BackIcon } from "@vicons/ionicons5";
|
||||
import { useStore } from "vuex";
|
||||
import XPageHeader from "@/components/PageHeader.vue";
|
||||
import XCode from "@/components/Code.vue";
|
||||
import XPanel from "@/components/Panel.vue";
|
||||
@ -86,6 +90,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const model = ref({} as Container);
|
||||
const raw = ref('');
|
||||
const node = route.params.node as string || '';
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
NInputGroup,
|
||||
NInputGroupLabel,
|
||||
} from "naive-ui";
|
||||
import { useStore } from "vuex";
|
||||
import 'xterm/css/xterm.css'
|
||||
import { Terminal } from 'xterm'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
@ -26,6 +27,7 @@ import { AttachAddon } from 'xterm-addon-attach'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const store = useStore()
|
||||
const props = defineProps({
|
||||
node: {
|
||||
type: String,
|
||||
@ -49,9 +51,9 @@ function connect() {
|
||||
|
||||
active.value = true
|
||||
let protocol = (location.protocol === "https:") ? "wss://" : "ws://";
|
||||
let host = import.meta.env.DEV ? 'localhost:8002' : location.host;
|
||||
let host = import.meta.env.DEV ? 'localhost:8001' : location.host;
|
||||
let cmd = encodeURIComponent(command.value)
|
||||
socket = new WebSocket(`${protocol}${host}/api/container/connect?node=${props.node}&id=${props.id}&cmd=${cmd}`);
|
||||
socket = new WebSocket(`${protocol}${host}/api/container/connect?token=${store.state.user.token}&node=${props.node}&id=${props.id}&cmd=${cmd}`);
|
||||
socket.onopen = () => {
|
||||
const fit = new FitAddon();
|
||||
term = new Terminal({ fontSize: 14, cursorBlink: true });
|
||||
|
@ -27,7 +27,11 @@
|
||||
<td width="75" style="font-weight: 500">{{ t('objects.' + g.key) }}</td>
|
||||
<td>
|
||||
<n-space item-style="display: flex;">
|
||||
<n-checkbox :value="p.key" :label="t('perms.' + p.perm)" v-for="p in g.items" />
|
||||
<n-checkbox
|
||||
:value="g.key + '.' + action"
|
||||
:label="t('perms.' + action)"
|
||||
v-for="action in g.actions"
|
||||
/>
|
||||
</n-space>
|
||||
</td>
|
||||
<td width="100">
|
||||
@ -112,18 +116,21 @@ const { submit, submiting } = useForm(form, () => roleApi.save(model.value), ()
|
||||
|
||||
function checkGroup(key: string, checked: boolean = true) {
|
||||
const g = perms.find(g => g.key === key)
|
||||
if (g) {
|
||||
g.items.forEach(p => {
|
||||
if (checked) {
|
||||
!model.value.perms.includes(p.key) && model.value.perms.push(p.key)
|
||||
} else {
|
||||
const index = model.value.perms.indexOf(p.key)
|
||||
if (index !== -1) {
|
||||
model.value.perms.splice(index, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!g) {
|
||||
return
|
||||
}
|
||||
|
||||
g.actions.forEach(action => {
|
||||
const perm = g.key + '.' + action
|
||||
if (checked) {
|
||||
!model.value.perms.includes(perm) && model.value.perms.push(perm)
|
||||
} else {
|
||||
const index = model.value.perms.indexOf(perm)
|
||||
if (index !== -1) {
|
||||
model.value.perms.splice(index, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
|
@ -33,14 +33,14 @@
|
||||
<x-description-item :label="t('fields.updated_at')">
|
||||
<n-time :time="model.updatedAt" format="y-MM-dd HH:mm:ss" />
|
||||
</x-description-item>
|
||||
<x-description-item :span="2" :label="t('fields.perms')">
|
||||
<n-grid cols="1 480:2 960:3 1440:4" x-gap="6">
|
||||
</x-description>
|
||||
<x-panel :title="t('fields.perms')">
|
||||
<n-grid cols="1 640:2 960:3 1440:4" x-gap="6" y-gap="6">
|
||||
<n-gi span="1" v-for="g in ps">
|
||||
<x-pair-tag type="warning" :label="g.group" :value="g.items" />
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</x-description-item>
|
||||
</x-description>
|
||||
</x-panel>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
@ -57,6 +57,7 @@ import {
|
||||
import { useRoute } from "vue-router";
|
||||
import { ArrowBackCircleOutline as BackIcon } from "@vicons/ionicons5";
|
||||
import XPageHeader from "@/components/PageHeader.vue";
|
||||
import XPanel from "@/components/Panel.vue";
|
||||
import XPairTag from "@/components/PairTag.vue";
|
||||
import XAnchor from "@/components/Anchor.vue";
|
||||
import { XDescription, XDescriptionItem } from "@/components/description";
|
||||
@ -72,7 +73,7 @@ const ps = computed(() => {
|
||||
const set = new Set(model.value.perms)
|
||||
const arr: any = []
|
||||
perms.forEach(g => {
|
||||
const items = g.items.filter(p => set.has(p.key)).map(p => t('perms.' + p.perm))
|
||||
const items = g.actions.filter(action => set.has(g.key + '.' + action)).map(p => t('perms.' + p))
|
||||
items.length && arr.push({ group: t('objects.' + g.key), items: items })
|
||||
})
|
||||
return arr
|
||||
|
@ -471,7 +471,8 @@
|
||||
</n-table>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="logs" :tab="t('fields.logs')" display-directive="show:lazy">
|
||||
<x-logs type="service" :id="service.name"></x-logs>
|
||||
<x-logs type="service" :id="service.name" v-if="store.getters.allow('service.logs')"></x-logs>
|
||||
<n-alert type="info" v-else>{{ t('texts.403') }}</n-alert>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="status" :tab="t('fields.status')">
|
||||
<x-dashboard type="service" :name="service.name" />
|
||||
@ -491,8 +492,10 @@ import {
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NInputNumber,
|
||||
NAlert,
|
||||
} from "naive-ui";
|
||||
import { ArrowBackCircleOutline as BackIcon } from "@vicons/ionicons5";
|
||||
import { useStore } from "vuex";
|
||||
import XPageHeader from "@/components/PageHeader.vue";
|
||||
import XAnchor from "@/components/Anchor.vue";
|
||||
import XPairTag from "@/components/PairTag.vue";
|
||||
@ -513,6 +516,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const service = ref({
|
||||
resource: {
|
||||
limit: {},
|
||||
|
@ -93,7 +93,8 @@
|
||||
<x-code :code="raw" language="json" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="logs" :tab="t('fields.logs')" display-directive="show:lazy">
|
||||
<x-logs type="task" :id="route.params.id as string"></x-logs>
|
||||
<x-logs type="task" :id="route.params.id as string" v-if="store.getters.allow('task.logs')"></x-logs>
|
||||
<n-alert type="info" v-else>{{ t('texts.403') }}</n-alert>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
@ -110,8 +111,10 @@ import {
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NText,
|
||||
NAlert,
|
||||
} from "naive-ui";
|
||||
import { ArrowBackCircleOutline as BackIcon } from "@vicons/ionicons5";
|
||||
import { useStore } from "vuex";
|
||||
import XPageHeader from "@/components/PageHeader.vue";
|
||||
import XAnchor from "@/components/Anchor.vue";
|
||||
import XCode from "@/components/Code.vue";
|
||||
@ -125,6 +128,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const model = ref({} as Task);
|
||||
const raw = ref('');
|
||||
|
||||
|
@ -78,7 +78,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { onMounted, reactive, watch } from "vue";
|
||||
import {
|
||||
NButton,
|
||||
NTag,
|
||||
@ -110,5 +110,7 @@ async function fetchData() {
|
||||
roles.data?.forEach(r => model.roles.set(r.id, r.name))
|
||||
}
|
||||
|
||||
watch(() => route.params.id, fetchData)
|
||||
|
||||
onMounted(fetchData);
|
||||
</script>
|
@ -23,6 +23,9 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'home',
|
||||
path: "/",
|
||||
component: () => import('../pages/Home.vue'),
|
||||
meta: {
|
||||
auth: '?',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'login',
|
||||
@ -30,7 +33,7 @@ const routes: RouteRecordRaw[] = [
|
||||
component: LoginPage,
|
||||
meta: {
|
||||
layout: "empty",
|
||||
anonymous: true,
|
||||
auth: '*',
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -39,68 +42,104 @@ const routes: RouteRecordRaw[] = [
|
||||
component: InitPage,
|
||||
meta: {
|
||||
layout: "empty",
|
||||
anonymous: true,
|
||||
auth: '*',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'profile',
|
||||
path: "/profile",
|
||||
component: () => import('../pages/Profile.vue'),
|
||||
meta: {
|
||||
auth: '?',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'node_list',
|
||||
path: "/swarm/nodes",
|
||||
component: () => import('../pages/node/List.vue'),
|
||||
meta: {
|
||||
auth: 'node.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'node_detail',
|
||||
path: "/swarm/nodes/:id",
|
||||
component: () => import('../pages/node/View.vue'),
|
||||
meta: {
|
||||
auth: 'node.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'node_edit',
|
||||
path: "/swarm/nodes/:id/edit",
|
||||
component: () => import('../pages/node/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'node.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'registry_list',
|
||||
path: "/swarm/registries",
|
||||
component: () => import('../pages/registry/List.vue'),
|
||||
meta: {
|
||||
auth: 'registry.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'registry_detail',
|
||||
path: "/swarm/registries/:id",
|
||||
component: () => import('../pages/registry/View.vue'),
|
||||
meta: {
|
||||
auth: 'registry.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'registry_new',
|
||||
path: "/swarm/registries/new",
|
||||
component: () => import('../pages/registry/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'registry.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'registry_edit',
|
||||
path: "/swarm/registries/:id/edit",
|
||||
component: () => import('../pages/registry/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'registry.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'network_list',
|
||||
path: "/swarm/networks",
|
||||
component: () => import('../pages/network/List.vue'),
|
||||
meta: {
|
||||
auth: 'network.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'network_new',
|
||||
path: "/swarm/networks/new",
|
||||
component: () => import('../pages/network/New.vue'),
|
||||
meta: {
|
||||
auth: 'network.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'network_detail',
|
||||
path: "/swarm/networks/:name",
|
||||
component: () => import('../pages/network/View.vue'),
|
||||
meta: {
|
||||
auth: 'network.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "service_list",
|
||||
path: "/swarm/services",
|
||||
component: () => import('../pages/service/List.vue'),
|
||||
meta: {
|
||||
auth: 'service.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "service_detail",
|
||||
@ -130,176 +169,281 @@ const routes: RouteRecordRaw[] = [
|
||||
name: "task_list",
|
||||
path: "/swarm/tasks",
|
||||
component: () => import('../pages/task/List.vue'),
|
||||
meta: {
|
||||
auth: 'task.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "task_detail",
|
||||
path: "/swarm/tasks/:id",
|
||||
component: () => import('../pages/task/View.vue'),
|
||||
meta: {
|
||||
auth: 'task.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "config_list",
|
||||
path: "/swarm/configs",
|
||||
component: () => import('../pages/config/List.vue'),
|
||||
meta: {
|
||||
auth: 'config.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "config_detail",
|
||||
path: "/swarm/configs/:id",
|
||||
component: () => import('../pages/config/View.vue'),
|
||||
meta: {
|
||||
auth: 'config.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "config_new",
|
||||
path: "/swarm/configs/new",
|
||||
component: () => import('../pages/config/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'config.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "config_edit",
|
||||
path: "/swarm/configs/:id/edit",
|
||||
component: () => import('../pages/config/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'config.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "secret_list",
|
||||
path: "/swarm/secrets",
|
||||
component: () => import('../pages/secret/List.vue'),
|
||||
meta: {
|
||||
auth: 'secret.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "secret_detail",
|
||||
path: "/swarm/secrets/:id",
|
||||
component: () => import('../pages/secret/View.vue'),
|
||||
meta: {
|
||||
auth: 'secret.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "secret_new",
|
||||
path: "/swarm/secrets/new",
|
||||
component: () => import('../pages/secret/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'secret.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "secret_edit",
|
||||
path: "/swarm/secrets/:id/edit",
|
||||
component: () => import('../pages/secret/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'secret.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "stack_list",
|
||||
path: "/swarm/stacks",
|
||||
component: () => import('../pages/stack/List.vue'),
|
||||
meta: {
|
||||
auth: 'stack.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "stack_detail",
|
||||
path: "/swarm/stacks/:name",
|
||||
component: () => import('../pages/stack/View.vue'),
|
||||
meta: {
|
||||
auth: 'stack.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "stack_new",
|
||||
path: "/swarm/stacks/new",
|
||||
component: () => import('../pages/stack/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'stack.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "stack_edit",
|
||||
path: "/swarm/stacks/:name/edit",
|
||||
component: () => import('../pages/stack/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'stack.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "image_list",
|
||||
path: "/local/images",
|
||||
component: () => import('../pages/image/List.vue'),
|
||||
meta: {
|
||||
auth: 'image.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "image_detail",
|
||||
path: "/local/images/:node/:id",
|
||||
component: () => import('../pages/image/View.vue'),
|
||||
meta: {
|
||||
auth: 'image.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "container_list",
|
||||
path: "/local/containers",
|
||||
component: () => import('../pages/container/List.vue'),
|
||||
meta: {
|
||||
auth: 'container.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "container_detail",
|
||||
path: "/local/containers/:node/:id",
|
||||
component: () => import('../pages/container/View.vue'),
|
||||
meta: {
|
||||
auth: 'container.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "volume_list",
|
||||
path: "/local/volumes",
|
||||
component: () => import('../pages/volume/List.vue'),
|
||||
meta: {
|
||||
auth: 'volume.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "volume_detail",
|
||||
path: "/local/volumes/:node/:name",
|
||||
component: () => import('../pages/volume/View.vue'),
|
||||
meta: {
|
||||
auth: 'volume.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "volume_new",
|
||||
path: "/local/volumes/:node/new",
|
||||
component: () => import('../pages/volume/New.vue'),
|
||||
meta: {
|
||||
auth: 'volume.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "user_list",
|
||||
path: "/system/users",
|
||||
component: () => import('../pages/user/List.vue'),
|
||||
meta: {
|
||||
auth: 'user.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "user_new",
|
||||
path: "/system/users/new",
|
||||
component: () => import('../pages/user/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'user.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "user_detail",
|
||||
path: "/system/users/:id",
|
||||
component: () => import('../pages/user/View.vue'),
|
||||
meta: {
|
||||
auth: 'user.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "user_edit",
|
||||
path: "/system/users/:id/edit",
|
||||
component: () => import('../pages/user/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'user.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "role_list",
|
||||
path: "/system/roles",
|
||||
component: () => import('../pages/role/List.vue'),
|
||||
meta: {
|
||||
auth: 'role.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "role_new",
|
||||
path: "/system/roles/new",
|
||||
component: () => import('../pages/role/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'role.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "role_detail",
|
||||
path: "/system/roles/:id",
|
||||
component: () => import('../pages/role/View.vue'),
|
||||
meta: {
|
||||
auth: 'role.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "role_edit",
|
||||
path: "/system/roles/:id/edit",
|
||||
component: () => import('../pages/role/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'role.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "event_list",
|
||||
path: "/system/events",
|
||||
component: () => import('../pages/event/List.vue'),
|
||||
meta: {
|
||||
auth: 'event.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "chart_list",
|
||||
path: "/system/charts",
|
||||
component: () => import('../pages/chart/List.vue'),
|
||||
meta: {
|
||||
auth: 'chart.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "chart_detail",
|
||||
path: "/system/charts/:id",
|
||||
component: () => import('../pages/chart/View.vue'),
|
||||
meta: {
|
||||
auth: 'chart.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "chart_new",
|
||||
path: "/system/charts/new",
|
||||
component: () => import('../pages/chart/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'chart.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "chart_edit",
|
||||
path: "/system/charts/:id/edit",
|
||||
component: () => import('../pages/chart/Edit.vue'),
|
||||
meta: {
|
||||
auth: 'chart.edit',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "setting",
|
||||
path: "/system/settings",
|
||||
component: () => import('../pages/setting/Setting.vue'),
|
||||
meta: {
|
||||
auth: 'setting.view',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '403',
|
||||
@ -307,7 +451,7 @@ const routes: RouteRecordRaw[] = [
|
||||
component: ForbiddenPage,
|
||||
meta: {
|
||||
layout: "simple",
|
||||
anonymous: true,
|
||||
auth: '*',
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -316,7 +460,7 @@ const routes: RouteRecordRaw[] = [
|
||||
component: NotFoundPage,
|
||||
meta: {
|
||||
layout: "simple",
|
||||
anonymous: true,
|
||||
auth: '*',
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -338,13 +482,15 @@ function createSiteRouter() {
|
||||
window.document.title = t(`titles.${to.name as string}`) + ' - Swirl'
|
||||
}
|
||||
|
||||
if (to.matched.some(record => !record.meta.anonymous)) {
|
||||
// this route requires auth, if not logged in, redirect to login page.
|
||||
const auth = to.meta.auth || '*'
|
||||
if (auth !== '*') {
|
||||
if (store.getters.anonymous) {
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath }
|
||||
})
|
||||
next({ name: 'login', query: { redirect: to.fullPath } })
|
||||
return
|
||||
}
|
||||
|
||||
if (auth !== '?' && !store.getters.allow(auth)) {
|
||||
next({ name: '403' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,14 @@ import { Mutations } from "./mutations";
|
||||
|
||||
const debug = import.meta.env.DEV
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
token: string;
|
||||
perms: Set<string>;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
name: string | null;
|
||||
token: string | null;
|
||||
user?: User | null;
|
||||
preference: {
|
||||
theme: string | null;
|
||||
locale: string | null;
|
||||
@ -13,21 +18,23 @@ export interface State {
|
||||
ajaxLoading: boolean;
|
||||
}
|
||||
|
||||
function initState(): State {
|
||||
const state: any = {
|
||||
name: localStorage.getItem("name"),
|
||||
token: localStorage.getItem("token"),
|
||||
ajaxLoading: false,
|
||||
}
|
||||
|
||||
const locale = navigator.language.startsWith('zh') ? 'zh' : 'en'
|
||||
state.preference = { theme: 'light', locale: locale }
|
||||
function loadObject(key: string) {
|
||||
let value = null
|
||||
try {
|
||||
state.preference = Object.assign(state.preference, JSON.parse(localStorage.getItem("preference") as string))
|
||||
value = JSON.parse(localStorage.getItem(key) as string)
|
||||
} catch {
|
||||
}
|
||||
|
||||
return state
|
||||
return value
|
||||
}
|
||||
|
||||
function initState(): State {
|
||||
const user = Object.assign({}, loadObject('user'))
|
||||
const locale = navigator.language.startsWith('zh') ? 'zh' : 'en'
|
||||
return {
|
||||
user: { perms: new Set(user.perms), name: user.name, token: user.token },
|
||||
preference: Object.assign({ theme: 'light', locale: locale }, loadObject('preference')),
|
||||
ajaxLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const store = createStore<State>({
|
||||
@ -35,25 +42,20 @@ export const store = createStore<State>({
|
||||
state: initState(),
|
||||
getters: {
|
||||
anonymous(state) {
|
||||
return !state.token
|
||||
}
|
||||
return !state.user?.token
|
||||
},
|
||||
allow(state) {
|
||||
return (perm: string) => state.user?.perms?.has('*') || state.user?.perms?.has(perm)
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
[Mutations.Login](state, user) {
|
||||
localStorage.setItem("name", user.name);
|
||||
localStorage.setItem("token", user.token);
|
||||
state.name = user.name;
|
||||
state.token = user.token;
|
||||
},
|
||||
[Mutations.Logout](state) {
|
||||
localStorage.removeItem("name");
|
||||
localStorage.removeItem("token");
|
||||
state.name = null;
|
||||
state.token = null;
|
||||
localStorage.removeItem("user");
|
||||
state.user = null;
|
||||
},
|
||||
[Mutations.SetToken](state, token) {
|
||||
localStorage.setItem("token", token);
|
||||
state.token = token;
|
||||
[Mutations.SetUser](state, user) {
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
state.user = { perms: new Set(user.perms), name: user.name, token: user.token };
|
||||
},
|
||||
[Mutations.SetPreference](state, preference) {
|
||||
localStorage.setItem("preference", JSON.stringify(preference));
|
||||
|
@ -1,7 +1,6 @@
|
||||
export enum Mutations {
|
||||
Login = "LOGIN",
|
||||
Logout = "LOGOUT",
|
||||
SetToken = "SET_TOKEN",
|
||||
SetPreference = "SET_THEME",
|
||||
SetUser = "SET_USER",
|
||||
SetPreference = "SET_PREFERENCE",
|
||||
SetAjaxLoading = "SET_AJAX_LOADING",
|
||||
}
|
@ -1,137 +1,70 @@
|
||||
export const perms = [
|
||||
{
|
||||
key: 'registry',
|
||||
items: [
|
||||
{ key: "registry.view", perm: "view" },
|
||||
{ key: "registry.edit", perm: "edit" },
|
||||
{ key: "registry.delete", perm: "delete" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
{
|
||||
key: 'node',
|
||||
items: [
|
||||
{ key: "node.view", perm: "view" },
|
||||
{ key: "node.edit", perm: "edit" },
|
||||
{ key: "node.delete", perm: "delete" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
{
|
||||
key: 'network',
|
||||
items: [
|
||||
{ key: "network.view", perm: "view" },
|
||||
{ key: "network.edit", perm: "edit" },
|
||||
{ key: "network.delete", perm: "delete" },
|
||||
{ key: "network.disconnect", perm: "disconnect" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete', 'disconnect'],
|
||||
},
|
||||
{
|
||||
key: 'service',
|
||||
items: [
|
||||
{ key: "service.view", perm: "view" },
|
||||
{ key: "service.edit", perm: "edit" },
|
||||
{ key: "service.delete", perm: "delete" },
|
||||
{ key: "service.restart", perm: "restart" },
|
||||
{ key: "service.rollback", perm: "rollback" },
|
||||
{ key: "service.logs", perm: "logs" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete', 'restart', 'rollback', 'logs'],
|
||||
},
|
||||
{
|
||||
key: 'task',
|
||||
items: [
|
||||
{ key: "task.view", perm: "view" },
|
||||
{ key: "task.logs", perm: "logs" },
|
||||
],
|
||||
actions: ['view', 'logs'],
|
||||
},
|
||||
{
|
||||
key: 'stack',
|
||||
items: [
|
||||
{ key: "stack.view", perm: "view" },
|
||||
{ key: "stack.edit", perm: "edit" },
|
||||
{ key: "stack.delete", perm: "delete" },
|
||||
{ key: "stack.deploy", perm: "deploy" },
|
||||
{ key: "stack.shutdown", perm: "shutdown" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete', 'deploy', 'shutdown'],
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
items: [
|
||||
{ key: "config.view", perm: "view" },
|
||||
{ key: "config.edit", perm: "edit" },
|
||||
{ key: "config.delete", perm: "delete" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
{
|
||||
key: 'secret',
|
||||
items: [
|
||||
{ key: "secret.view", perm: "view" },
|
||||
{ key: "secret.edit", perm: "edit" },
|
||||
{ key: "secret.delete", perm: "delete" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
{
|
||||
key: 'image',
|
||||
items: [
|
||||
{ key: "image.view", perm: "view" },
|
||||
{ key: "image.delete", perm: "delete" },
|
||||
],
|
||||
actions: ['view', 'delete'],
|
||||
},
|
||||
{
|
||||
key: 'container',
|
||||
items: [
|
||||
{ key: "container.view", perm: "view" },
|
||||
{ key: "container.delete", perm: "delete" },
|
||||
{ key: "container.logs", perm: "logs" },
|
||||
],
|
||||
actions: ['view', 'delete', 'logs', 'execute'],
|
||||
},
|
||||
{
|
||||
key: 'volume',
|
||||
items: [
|
||||
{ key: "volume.view", perm: "view" },
|
||||
{ key: "volume.edit", perm: "edit" },
|
||||
{ key: "volume.delete", perm: "delete" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
items: [
|
||||
{ key: "user.view", perm: "view" },
|
||||
{ key: "user.edit", perm: "edit" },
|
||||
{ key: "user.delete", perm: "delete" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
items: [
|
||||
{ key: "role.view", perm: "view" },
|
||||
{ key: "role.edit", perm: "edit" },
|
||||
{ key: "role.delete", perm: "delete" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
{
|
||||
key: 'chart',
|
||||
items: [
|
||||
{ key: "chart.view", perm: "view" },
|
||||
{ key: "chart.edit", perm: "edit" },
|
||||
{ key: "chart.delete", perm: "delete" },
|
||||
{ key: "chart.dashboard", perm: "dashboard" },
|
||||
],
|
||||
actions: ['view', 'edit', 'delete'],
|
||||
},
|
||||
{
|
||||
key: 'dashboard',
|
||||
actions: ['edit'],
|
||||
},
|
||||
{
|
||||
key: 'event',
|
||||
items: [
|
||||
{ key: "event.view", perm: "view" },
|
||||
],
|
||||
actions: ['view'],
|
||||
},
|
||||
{
|
||||
key: 'setting',
|
||||
items: [
|
||||
{ key: "setting.view", perm: "view" },
|
||||
{ key: "setting.edit", perm: "edit" },
|
||||
],
|
||||
actions: ['view', 'edit'],
|
||||
},
|
||||
]
|
||||
|
||||
function contains(arr1: string[], arr2: string[]): boolean {
|
||||
const set = new Set(arr1);
|
||||
return arr2.every(s => set.has(s))
|
||||
}
|
@ -28,7 +28,7 @@ export default defineConfig({
|
||||
port: 3002,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8002',
|
||||
target: 'http://localhost:8001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user