Refactor authentication & authorization modules

This commit is contained in:
cuigh 2018-02-24 12:26:38 +08:00
parent 81081c8164
commit 5b60a024c5
14 changed files with 340 additions and 335 deletions

98
Gopkg.lock generated
View File

@ -22,25 +22,87 @@
[[projects]]
branch = "master"
name = "github.com/cuigh/auxo"
packages = [".","app","app/flag","byte/size","cache","cache/memory","config","data","data/guid","data/valid","encoding/yaml","errors","ext/files","ext/reflects","ext/texts","ext/times","log","log/console","log/file","net/web","net/web/filter","net/web/filter/auth","net/web/renderer","net/web/renderer/jet","net/web/router","security","security/passwd","util/cast","util/debug","util/i18n","util/lazy"]
revision = "f599f84ec7ef56f01457d0fb0d8eeb9d8c100b43"
packages = [
".",
"app",
"app/flag",
"byte/size",
"cache",
"cache/memory",
"config",
"data",
"data/guid",
"data/set",
"data/valid",
"encoding/yaml",
"errors",
"ext/files",
"ext/reflects",
"ext/texts",
"ext/times",
"log",
"log/console",
"log/file",
"net/web",
"net/web/filter",
"net/web/filter/auth",
"net/web/renderer",
"net/web/renderer/jet",
"net/web/router",
"security",
"security/certify",
"security/certify/ldap",
"security/passwd",
"util/cast",
"util/debug",
"util/i18n",
"util/lazy"
]
revision = "ac7f3a5d4d43fd755008c87525a95698544843ec"
[[projects]]
branch = "master"
name = "github.com/docker/distribution"
packages = [".","digestset","reference"]
packages = [
".",
"digestset",
"reference"
]
revision = "e5b5e44386f7964a1f010a2b8b7687d073215bbb"
[[projects]]
branch = "master"
name = "github.com/docker/docker"
packages = ["api","api/types","api/types/blkiodev","api/types/container","api/types/events","api/types/filters","api/types/image","api/types/mount","api/types/network","api/types/registry","api/types/strslice","api/types/swarm","api/types/swarm/runtime","api/types/time","api/types/versions","api/types/volume","client","pkg/stdcopy"]
packages = [
"api",
"api/types",
"api/types/blkiodev",
"api/types/container",
"api/types/events",
"api/types/filters",
"api/types/image",
"api/types/mount",
"api/types/network",
"api/types/registry",
"api/types/strslice",
"api/types/swarm",
"api/types/swarm/runtime",
"api/types/time",
"api/types/versions",
"api/types/volume",
"client",
"pkg/stdcopy"
]
revision = "f62aeae97791b4c2318791a9b621d7da4c17ac32"
source = "https://github.com/moby/moby.git"
[[projects]]
name = "github.com/docker/go-connections"
packages = ["nat","sockets","tlsconfig"]
packages = [
"nat",
"sockets",
"tlsconfig"
]
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
version = "v0.3.0"
@ -53,7 +115,13 @@
[[projects]]
branch = "master"
name = "github.com/globalsign/mgo"
packages = [".","bson","internal/json","internal/sasl","internal/scram"]
packages = [
".",
"bson",
"internal/json",
"internal/sasl",
"internal/scram"
]
revision = "896bbb89d21253a28cd5a7f8b81fe091410cb94d"
[[projects]]
@ -88,7 +156,10 @@
[[projects]]
name = "github.com/opencontainers/image-spec"
packages = ["specs-go","specs-go/v1"]
packages = [
"specs-go",
"specs-go/v1"
]
revision = "d60099175f88c47cd379c4738d158884749ed235"
version = "v1.0.1"
@ -101,13 +172,20 @@
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["acme","acme/autocert"]
packages = [
"acme",
"acme/autocert"
]
revision = "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context","context/ctxhttp","proxy"]
packages = [
"context",
"context/ctxhttp",
"proxy"
]
revision = "a337091b0525af65de94df2eb7e98bd9962dcbe2"
[[projects]]
@ -131,6 +209,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "2e2023629c134da8099f44e6d2719a5bc407d2b350e75f6f79a35ade95859ebd"
inputs-digest = "aae5ea1cf0fb5fd50cb9659fb0abe58fc9546231ec91f9df9148c5df3e3cc194"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -17,10 +17,6 @@ required = ["github.com/docker/distribution"]
name = "github.com/docker/go-units"
version = "0.3.2"
[[constraint]]
name = "github.com/go-ldap/ldap"
version = "2.5.0"
[[constraint]]
name = "github.com/mattn/go-shellwords"
version = "1.0.3"

View File

@ -22,7 +22,7 @@ import (
)
// ServiceList return service list.
func ServiceList(name string, pageIndex, pageSize int) (infos []*model.ServiceListInfo, totalCount int, err error) {
func ServiceList(name string, pageIndex, pageSize int) (infos []*model.ServiceListInfo, totalCount int, err error) { // nolint: gocyclo
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var (
services []swarm.Service
@ -118,7 +118,7 @@ func ServiceInspect(name string) (service swarm.Service, raw []byte, err error)
}
// ServiceUpdate update a service.
func ServiceUpdate(info *model.ServiceInfo) error {
func ServiceUpdate(info *model.ServiceInfo) error { // nolint: gocyclo
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
service, _, err := cli.ServiceInspectWithRaw(ctx, info.Name, types.ServiceInspectOptions{})
if err != nil {
@ -343,7 +343,7 @@ func ServiceScale(name string, count uint64) error {
}
// ServiceCreate create a service.
func ServiceCreate(info *model.ServiceInfo) error {
func ServiceCreate(info *model.ServiceInfo) error { // nolint: gocyclo
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
service := swarm.ServiceSpec{
Annotations: swarm.Annotations{
@ -576,129 +576,129 @@ func ServiceCommand(name string) (cmd string, err error) { // nolint: gocyclo
}
si := model.NewServiceInfo(service)
b := texts.GetBuilder()
b.Append("docker service create --name ", si.Name)
b := texts.Builder{}
b.WriteString("docker service create --name ", si.Name)
if si.Mode == "global" {
b.Append(" --mode global")
b.WriteString(" --mode global")
} else if si.Replicas > 1 {
b.AppendFormat(" --replicas %d", si.Replicas)
b.WriteFormat(" --replicas %d", si.Replicas)
}
for _, n := range service.Spec.TaskTemplate.Networks {
var network types.NetworkResource
network, err = cli.NetworkInspect(ctx, n.Target, types.NetworkInspectOptions{})
b.Append(" --network ", network.Name)
b.WriteString(" --network ", network.Name)
}
if len(si.Endpoint.Ports) > 0 {
if si.Endpoint.Mode == swarm.ResolutionModeDNSRR {
b.AppendFormat(" --endpoint-mode %s", si.Endpoint.Mode)
b.WriteFormat(" --endpoint-mode %s", si.Endpoint.Mode)
}
for _, p := range si.Endpoint.Ports {
b.AppendFormat(" --publish mode=%s,target=%d,published=%d,protocol=%s", p.PublishMode, p.TargetPort, p.PublishedPort, p.Protocol)
b.WriteFormat(" --publish mode=%s,target=%d,published=%d,protocol=%s", p.PublishMode, p.TargetPort, p.PublishedPort, p.Protocol)
}
}
for _, c := range si.Placement.Constraints {
b.AppendFormat(" --constraint '%s'", c.ToConstraint())
b.WriteFormat(" --constraint '%s'", c.ToConstraint())
}
for _, p := range si.Placement.Preferences {
b.AppendFormat(" --placement-pref '%s'", p.Spread)
b.WriteFormat(" --placement-pref '%s'", p.Spread)
}
for _, e := range si.Environments {
b.AppendFormat(" --env '%s=%s'", e.Name, e.Value)
b.WriteFormat(" --env '%s=%s'", e.Name, e.Value)
}
for _, l := range si.ServiceLabels {
b.AppendFormat(" --label '%s=%s'", l.Name, l.Value)
b.WriteFormat(" --label '%s=%s'", l.Name, l.Value)
}
for _, l := range si.ContainerLabels {
b.AppendFormat(" --container-label '%s=%s'", l.Name, l.Value)
b.WriteFormat(" --container-label '%s=%s'", l.Name, l.Value)
}
for _, mnt := range si.Mounts {
// todo: add mnt.Propagation
b.AppendFormat(" --mount type=%s,source=%s,destination=%s", mnt.Type, mnt.Source, mnt.Target)
b.WriteFormat(" --mount type=%s,source=%s,destination=%s", mnt.Type, mnt.Source, mnt.Target)
if mnt.ReadOnly {
b.Append(",ro=1")
b.WriteString(",ro=1")
}
}
// todo: uid/gid
for _, c := range si.Configs {
b.AppendFormat(" --config source=%s,target=%s,mode=0%d", c.Name, c.FileName, c.Mode)
b.WriteFormat(" --config source=%s,target=%s,mode=0%d", c.Name, c.FileName, c.Mode)
}
for _, c := range si.Secrets {
b.AppendFormat(" --secret source=%s,target=%s,mode=0%d", c.Name, c.FileName, c.Mode)
b.WriteFormat(" --secret source=%s,target=%s,mode=0%d", c.Name, c.FileName, c.Mode)
}
if si.Resource.Limit.IsSet() {
if si.Resource.Limit.CPU > 0 {
b.AppendFormat(" --limit-cpu %.2f", si.Resource.Limit.CPU)
b.WriteFormat(" --limit-cpu %.2f", si.Resource.Limit.CPU)
}
if si.Resource.Limit.Memory != "" {
b.Append(" --limit-memory ", si.Resource.Limit.Memory)
b.WriteString(" --limit-memory ", si.Resource.Limit.Memory)
}
}
if si.Resource.Reserve.IsSet() {
if si.Resource.Reserve.CPU > 0 {
b.AppendFormat(" --reserve-cpu %.2f", si.Resource.Reserve.CPU)
b.WriteFormat(" --reserve-cpu %.2f", si.Resource.Reserve.CPU)
}
if si.Resource.Reserve.Memory != "" {
b.Append(" --reserve-memory ", si.Resource.Reserve.Memory)
b.WriteString(" --reserve-memory ", si.Resource.Reserve.Memory)
}
}
if si.LogDriver.Name != "" {
b.Append(" --log-driver ", si.LogDriver.Name)
b.WriteString(" --log-driver ", si.LogDriver.Name)
for _, opt := range si.LogDriver.Options {
b.AppendFormat(" --log-opt '%s=%s'", opt.Name, opt.Value)
b.WriteFormat(" --log-opt '%s=%s'", opt.Name, opt.Value)
}
}
// UpdatePolicy
if si.UpdatePolicy.Parallelism > 0 {
b.AppendFormat(" --update-parallelism %d", si.UpdatePolicy.Parallelism)
b.WriteFormat(" --update-parallelism %d", si.UpdatePolicy.Parallelism)
}
if si.UpdatePolicy.Delay != "" {
b.Append(" --update-delay ", si.UpdatePolicy.Delay)
b.WriteString(" --update-delay ", si.UpdatePolicy.Delay)
}
if si.UpdatePolicy.FailureAction != "" {
b.Append(" --update-failure-action ", si.UpdatePolicy.FailureAction)
b.WriteString(" --update-failure-action ", si.UpdatePolicy.FailureAction)
}
if si.UpdatePolicy.Order != "" {
b.Append(" --update-order ", si.UpdatePolicy.Order)
b.WriteString(" --update-order ", si.UpdatePolicy.Order)
}
// RollbackPolicy
if si.RollbackPolicy.Parallelism > 0 {
b.AppendFormat(" --rollback-parallelism %d", si.RollbackPolicy.Parallelism)
b.WriteFormat(" --rollback-parallelism %d", si.RollbackPolicy.Parallelism)
}
if si.RollbackPolicy.Delay != "" {
b.Append(" --rollback-delay ", si.RollbackPolicy.Delay)
b.WriteString(" --rollback-delay ", si.RollbackPolicy.Delay)
}
if si.RollbackPolicy.FailureAction != "" {
b.Append(" --rollback-failure-action ", si.RollbackPolicy.FailureAction)
b.WriteString(" --rollback-failure-action ", si.RollbackPolicy.FailureAction)
}
if si.RollbackPolicy.Order != "" {
b.Append(" --rollback-order ", si.RollbackPolicy.Order)
b.WriteString(" --rollback-order ", si.RollbackPolicy.Order)
}
// RestartPolicy
if si.RestartPolicy.Condition != "" {
b.AppendFormat(" --restart-condition %s", si.RestartPolicy.Condition)
b.WriteFormat(" --restart-condition %s", si.RestartPolicy.Condition)
}
if si.RestartPolicy.MaxAttempts > 0 {
b.AppendFormat(" --restart-max-attempts %d", si.RestartPolicy.MaxAttempts)
b.WriteFormat(" --restart-max-attempts %d", si.RestartPolicy.MaxAttempts)
}
if si.RestartPolicy.Delay != "" {
b.Append(" --restart-delay ", si.RestartPolicy.Delay)
b.WriteString(" --restart-delay ", si.RestartPolicy.Delay)
}
if si.RestartPolicy.Window != "" {
b.Append(" --restart-window ", si.RestartPolicy.Window)
b.WriteString(" --restart-window ", si.RestartPolicy.Window)
}
//b.Append(" --with-registry-auth")
//b.WriteString(" --with-registry-auth")
if si.Dir != "" {
b.Append(" --workdir ", si.Dir)
b.WriteString(" --workdir ", si.Dir)
}
if si.User != "" {
b.Append(" --user ", si.User)
b.WriteString(" --user ", si.User)
}
b.Append(" ", si.Image)
b.WriteString(" ", si.Image)
if si.Command != "" {
b.Append(" ", si.Command)
b.WriteString(" ", si.Command)
}
if si.Args != "" {
b.Append(" ", si.Args)
b.WriteString(" ", si.Args)
}
cmd = b.String()
return

View File

@ -2,7 +2,6 @@ package biz
import (
"net/http"
"strings"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/dao"
@ -39,6 +38,7 @@ func (b *permBiz) Update(perm *model.Perm, user web.User) (err error) {
return
}
// nolint: gocyclo
func (b *permBiz) Check(user web.User, scope string, resType, resID string) (err error) {
au := user.(*model.AuthUser)
if au.Admin() {
@ -71,17 +71,3 @@ func (b *permBiz) Check(user web.User, scope string, resType, resID string) (err
})
return
}
func (b *permBiz) Apply(next web.HandlerFunc) web.HandlerFunc {
return func(ctx web.Context) error {
opt := ctx.Handler().Option("perm")
if opt != "" {
array := strings.Split(opt, ",")
err := b.Check(ctx.User(), array[0], array[1], ctx.P(array[2]))
if err != nil {
return err
}
}
return next(ctx)
}
}

View File

@ -1,25 +1,17 @@
package biz
import (
"crypto/tls"
"fmt"
"net"
"time"
"github.com/cuigh/auxo/cache"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/auxo/log"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/auxo/security/passwd"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/model"
"github.com/go-ldap/ldap"
)
var ErrIncorrectAuth = errors.New("login name or password is incorrect")
var User = &userBiz{}
var User = &userBiz{} // nolint: golint
type userBiz struct {
}
@ -137,232 +129,25 @@ func (b *userBiz) Count() (count int, err error) {
return
}
func (b *userBiz) Login(name, pwd string) (token string, err error) {
func (b *userBiz) UpdateSession(id string) (token string, err error) {
session := &model.Session{
UserID: id,
Token: misc.NewID(),
UpdatedAt: time.Now(),
}
session.Expires = session.UpdatedAt.Add(time.Hour * 24)
do(func(d dao.Interface) {
var (
user *model.User
)
user, err = d.UserGetByName(name)
if err != nil {
return
}
if user == nil {
user = &model.User{
Type: model.UserTypeLDAP,
LoginName: name,
}
err = b.loginLDAP(d, user, pwd)
} else {
if user.Status == model.UserStatusBlocked {
err = fmt.Errorf("user %s is blocked", name)
return
}
if user.Type == model.UserTypeInternal {
err = b.loginInternal(user, pwd)
} else {
err = b.loginLDAP(d, user, pwd)
}
}
if err != nil {
return
}
session := &model.Session{
UserID: user.ID,
Token: misc.NewID(),
UpdatedAt: time.Now(),
}
session.Expires = session.UpdatedAt.Add(time.Hour * 24)
err = d.SessionUpdate(session)
if err != nil {
return
}
token = session.Token
// create event
Event.CreateAuthentication(model.EventActionLogin, user.ID, user.LoginName, user.Name)
})
return
}
func (b *userBiz) loginInternal(user *model.User, pwd string) error {
if !passwd.Validate(pwd, user.Password, user.Salt) {
return ErrIncorrectAuth
}
return nil
}
func (b *userBiz) loginLDAP(d dao.Interface, user *model.User, pwd string) error {
setting, err := Setting.Get()
if err != nil {
return err
}
if !setting.LDAP.Enabled {
return ErrIncorrectAuth
}
l, err := b.ldapDial(setting)
if err != nil {
return err
}
defer l.Close()
// bind
err = b.ldapBind(setting, l, user, pwd)
if err != nil {
log.Get("user").Error("LDAP > Failed to bind: ", err)
return ErrIncorrectAuth
}
// Stop here for an exist user because we only need validate password.
if user.ID != "" {
return nil
}
// If user wasn't exist, we need create it
entry, err := b.ldapSearchOne(setting, l, user.LoginName, setting.LDAP.NameAttr, setting.LDAP.EmailAttr)
if err != nil {
return err
}
user.Email = entry.GetAttributeValue(setting.LDAP.EmailAttr)
user.Name = entry.GetAttributeValue(setting.LDAP.NameAttr)
return b.Create(user, nil)
}
func (b *userBiz) ldapDial(setting *model.Setting) (*ldap.Conn, error) {
host, _, err := net.SplitHostPort(setting.LDAP.Address)
if err != nil {
return nil, err
}
// TODO: support tls cert and verification
tc := &tls.Config{
ServerName: host,
InsecureSkipVerify: true,
Certificates: nil,
}
if setting.LDAP.Security == model.LDAPSecurityTLS {
return ldap.DialTLS("tcp", setting.LDAP.Address, tc)
}
conn, err := ldap.Dial("tcp", setting.LDAP.Address)
if err != nil {
return nil, err
}
if setting.LDAP.Security == model.LDAPSecurityStartTLS {
if err = conn.StartTLS(tc); err != nil {
conn.Close()
log.Get("user").Error("LDAP > Failed to switch to TLS: ", err)
return nil, err
}
}
return conn, nil
}
func (b *userBiz) ldapBind(setting *model.Setting, l *ldap.Conn, user *model.User, pwd string) (err error) {
if setting.LDAP.Authentication == 0 {
// simple auth
err = l.Bind(fmt.Sprintf(setting.LDAP.UserDN, user.LoginName), pwd)
} else {
// bind auth
err = l.Bind(setting.LDAP.BindDN, setting.LDAP.BindPassword)
if err == nil {
var entry *ldap.Entry
entry, err = b.ldapSearchOne(setting, l, user.LoginName, "cn")
if err == nil {
err = l.Bind(entry.DN, pwd)
}
token = session.Token
}
}
return
}
func (b *userBiz) ldapSearchOne(setting *model.Setting, l *ldap.Conn, name string, attrs ...string) (entry *ldap.Entry, err error) {
filter := fmt.Sprintf(setting.LDAP.UserFilter, name)
req := ldap.NewSearchRequest(
setting.LDAP.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter, attrs, nil,
)
sr, err := l.Search(req)
if err != nil {
return nil, err
}
if length := len(sr.Entries); length == 0 {
return nil, errors.New("User not found with filter: " + filter)
} else if length > 1 {
return nil, errors.New("Found more than one account when searching user with filter: " + filter)
}
return sr.Entries[0], nil
}
// Identify authenticate user
func (b *userBiz) Identify(token string) (user web.User) {
const cacheKey = "auth_user"
do(func(d dao.Interface) {
var (
roles []*model.Role
role *model.Role
)
session, err := d.SessionGet(token)
if err != nil {
log.Get("user").Errorf("Load session failed: %v", err)
return
}
if session == nil || session.Expires.Before(time.Now()) {
return
}
value := cache.Get(cacheKey, session.UserID)
if !value.IsNil() {
user = &model.AuthUser{}
if err = value.Scan(user); err == nil {
return
}
log.Get("user").Warnf("Load auth user from cache failed: %v", err)
}
u, err := d.UserGetByID(session.UserID)
if err != nil {
log.Get("user").Errorf("Load user failed: %v", err)
return
}
if u == nil {
return
}
if len(u.Roles) > 0 {
roles = make([]*model.Role, len(u.Roles))
for i, id := range u.Roles {
role, err = d.RoleGet(id)
if err != nil {
return
} else if role != nil {
roles[i] = role
}
}
}
user = model.NewAuthUser(u, roles)
cache.Set(user, cacheKey, session.UserID)
})
return
}
// Authorize check permission of user
func (b *userBiz) Authorize(user web.User, h web.HandlerInfo) bool {
if au, ok := user.(*model.AuthUser); ok {
return au.IsAllowed(h.Name())
}
return false
func (b *userBiz) GetSession(token string) (session *model.Session, err error) {
do(func(d dao.Interface) {
session, err = d.SessionGet(token)
})
return
}

View File

@ -2,6 +2,7 @@ package controller
import (
"github.com/cuigh/auxo/data"
"github.com/cuigh/auxo/data/set"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/model"
@ -21,11 +22,7 @@ func permEdit(ctx web.Context, resType, resID, tpl string, m data.Map) error {
return err
}
checkedRoles := data.Set{}
checkedRoles.AddSlice(perm.Roles, func(i int) interface{} {
return perm.Roles[i]
})
checkedRoles := set.NewStringSet(perm.Roles...)
var users []*model.User
for _, id := range perm.Users {
var user *model.User

View File

@ -1,10 +1,11 @@
package controller
import (
"github.com/cuigh/auxo/data"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/model"
"github.com/cuigh/swirl/security"
)
// RoleController is a controller of user role
@ -42,7 +43,7 @@ func roleIndex(ctx web.Context) error {
}
func roleNew(ctx web.Context) error {
m := newModel(ctx).Set("Perms", misc.Perms)
m := newModel(ctx).Set("Perms", security.Perms)
return ctx.Render("system/role/new", m)
}
@ -73,9 +74,9 @@ func roleDetail(ctx web.Context) error {
perms := make(map[string]struct{})
for _, p := range role.Perms {
perms[p] = model.Placeholder
perms[p] = data.Empty
}
m := newModel(ctx).Set("Role", role).Set("Perms", misc.Perms).Set("CheckedPerms", perms)
m := newModel(ctx).Set("Role", role).Set("Perms", security.Perms).Set("CheckedPerms", perms)
return ctx.Render("system/role/detail", m)
}
@ -91,9 +92,9 @@ func roleEdit(ctx web.Context) error {
perms := make(map[string]struct{})
for _, p := range role.Perms {
perms[p] = model.Placeholder
perms[p] = data.Empty
}
m := newModel(ctx).Set("Role", role).Set("Perms", misc.Perms).Set("CheckedPerms", perms)
m := newModel(ctx).Set("Role", role).Set("Perms", security.Perms).Set("CheckedPerms", perms)
return ctx.Render("system/role/edit", m)
}

View File

@ -4,7 +4,7 @@ import (
"strconv"
"strings"
"github.com/cuigh/auxo/data"
"github.com/cuigh/auxo/data/set"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/auxo/util/cast"
@ -162,11 +162,7 @@ func serviceNew(ctx web.Context) error {
return err
}
checkedNetworks := data.NewSet()
checkedNetworks.AddSlice(info.Networks, func(i int) interface{} {
return info.Networks[i]
})
checkedNetworks := set.NewStringSet(info.Networks...)
m := newModel(ctx).Set("Service", info).Set("Registries", registries).
Set("Networks", networks).Set("CheckedNetworks", checkedNetworks).
Set("Secrets", secrets).Set("Configs", configs)
@ -221,8 +217,8 @@ func serviceEdit(ctx web.Context) error {
}
stack := service.Spec.Labels["com.docker.stack.namespace"]
checkedNetworks := data.NewSet()
checkedNetworks.AddSlice(service.Endpoint.VirtualIPs, func(i int) interface{} { return service.Endpoint.VirtualIPs[i].NetworkID })
checkedNetworks := set.StringSet{}
checkedNetworks.AddSlice(service.Endpoint.VirtualIPs, func(i int) string { return service.Endpoint.VirtualIPs[i].NetworkID })
m := newModel(ctx).Set("Service", model.NewServiceInfo(service)).Set("Stack", stack).
Set("Networks", networks).Set("CheckedNetworks", checkedNetworks).

View File

@ -3,7 +3,7 @@ package controller
import (
"encoding/json"
"github.com/cuigh/auxo/data"
"github.com/cuigh/auxo/data/set"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/biz/docker"
@ -73,7 +73,7 @@ func templateNew(ctx web.Context) error {
return err
}
m := newModel(ctx).Set("Action", "New").Set("Service", service).Set("Registries", registries).
Set("Networks", networks).Set("CheckedNetworks", data.Set{}).
Set("Networks", networks).Set("CheckedNetworks", set.StringSet{}).
Set("Secrets", secrets).Set("Configs", configs)
return ctx.Render("service/template/edit", m)
}
@ -143,9 +143,7 @@ func templateEdit(ctx web.Context) error {
return err
}
checkedNetworks := data.NewSet()
checkedNetworks.AddSlice(service.Networks, func(i int) interface{} { return service.Networks[i] })
checkedNetworks := set.NewStringSet(service.Networks...)
m := newModel(ctx).Set("Action", "Edit").Set("Service", service).Set("Registries", registries).
Set("Networks", networks).Set("CheckedNetworks", checkedNetworks).
Set("Secrets", secrets).Set("Configs", configs)

View File

@ -1,6 +1,7 @@
package controller
import (
"github.com/cuigh/auxo/data"
"github.com/cuigh/auxo/log"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
@ -129,7 +130,7 @@ func userEdit(ctx web.Context) error {
userRoles := make(map[string]struct{})
for _, id := range user.Roles {
userRoles[id] = model.Placeholder
userRoles[id] = data.Empty
}
m := newModel(ctx).Set("User", user).Set("Roles", roles).Set("UserRoles", userRoles)
return ctx.Render("system/user/edit", m)

11
main.go
View File

@ -19,13 +19,14 @@ import (
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/controller"
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/security"
)
func main() {
misc.BindOptions()
app.Name = "Swirl"
app.Version = "0.6.7"
app.Version = "0.6.8"
app.Desc = "A web management UI for Docker, focused on swarm cluster"
app.Action = func(ctx *app.Context) {
misc.LoadOptions()
@ -77,14 +78,14 @@ func server() *web.Server {
// create biz group
form := &auth.Form{
Identifier: biz.User.Identify,
Identifier: security.Identifier,
Timeout: time.Minute * 30,
SlidingExpiration: true,
}
g := ws.Group("", form, filter.NewAuthorizer(biz.User.Authorize))
g := ws.Group("", form, filter.NewAuthorizer(security.Checker))
// register auth handlers
g.Post("/login", form.LoginJSON(biz.User.Login), web.WithName("login"), web.WithAuthorize(web.AuthAnonymous))
g.Post("/login", form.LoginJSON(security.Validator(setting)), web.WithName("login"), web.WithAuthorize(web.AuthAnonymous))
g.Get("/logout", form.Logout, web.WithName("logout"), web.WithAuthorize(web.AuthAuthenticated))
// register controllers
@ -92,7 +93,7 @@ func server() *web.Server {
g.Handle("/profile", controller.Profile())
g.Handle("/registry", controller.Registry())
g.Handle("/node", controller.Node())
g.Handle("/service", controller.Service(), biz.Perm)
g.Handle("/service", controller.Service(), web.FilterFunc(security.Permiter))
g.Handle("/service/template", controller.Template())
g.Handle("/stack", controller.Stack())
g.Handle("/network", controller.Network())

View File

@ -2,6 +2,8 @@ package model
import (
"time"
"github.com/cuigh/auxo/data"
)
type UserType string
@ -13,8 +15,6 @@ const (
UserTypeLDAP UserType = "ldap"
)
var Placeholder = struct{}{}
type UserStatus int32
const (
@ -80,7 +80,7 @@ func NewAuthUser(user *User, roles []*Role) *AuthUser {
}
for _, role := range roles {
for _, perm := range role.Perms {
u.perms[perm] = Placeholder
u.perms[perm] = data.Empty
}
}
return u

132
security/auth.go Normal file
View File

@ -0,0 +1,132 @@
package security
import (
"time"
"github.com/cuigh/auxo/cache"
"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/model"
)
const pkgName = "swirl.security"
// Validator is the authenticator function of swirl.
func Validator(setting *model.Setting) func(name, pwd string) (ticket string, err error) {
ldapRealm := createLDAPRealm(setting)
return func(name, pwd string) (ticket string, err error) {
var (
su security.User
mu *model.User
)
// try find user first
mu, err = biz.User.GetByName(name)
if err != nil {
return
}
if mu != nil && mu.Type == model.UserTypeInternal { // internal user
if !passwd.Validate(pwd, mu.Password, mu.Salt) {
err = certify.ErrInvalidToken
}
} else if ldapRealm != nil { // user not exist or ldap user
su, err = ldapRealm.Login(certify.NewSimpleToken(name, pwd))
if err == nil && mu == nil { // create user if not exists
lu := su.(*ldap.User)
mu = &model.User{
Type: model.UserTypeLDAP,
LoginName: lu.LoginName(),
Name: lu.Name(),
Email: lu.Email(),
}
err = biz.User.Create(mu, nil)
}
}
// replace user session, one session is allowed per user.
if err == nil {
ticket, err = biz.User.UpdateSession(mu.ID)
}
// create event
if err == nil {
biz.Event.CreateAuthentication(model.EventActionLogin, mu.ID, mu.LoginName, mu.Name)
}
return
}
}
func createLDAPRealm(setting *model.Setting) certify.Realm {
if setting.LDAP.Enabled {
opts := []ldap.Option{
ldap.NameAttr(setting.LDAP.NameAttr),
ldap.EmailAttr(setting.LDAP.EmailAttr),
ldap.UserFilter(setting.LDAP.UserFilter),
ldap.Security(ldap.SecurityPolicy(setting.LDAP.Security)),
}
if setting.LDAP.Authentication == 1 {
opts = append(opts, ldap.Binding(setting.LDAP.BindDN, setting.LDAP.BindPassword))
}
return ldap.New(setting.LDAP.Address, setting.LDAP.BaseDN, setting.LDAP.UserDN, opts...)
}
return nil
}
// Identifier is used to identity user.
func Identifier(token string) (user web.User) {
const cacheKey = "auth_user"
session, err := biz.User.GetSession(token)
if err != nil {
log.Get(pkgName).Errorf("Load session failed: %v", err)
return
}
if session == nil || session.Expires.Before(time.Now()) {
return
}
// try find from cache first
value := cache.Get(cacheKey, session.UserID)
if !value.IsNil() {
user = &model.AuthUser{}
if err = value.Scan(user); err == nil {
return
}
log.Get(pkgName).Warnf("Load auth user from cache failed: %v", err)
}
u, err := biz.User.GetByID(session.UserID)
if err != nil {
log.Get(pkgName).Errorf("Load user failed: %v", err)
return
}
if u == nil {
return
}
var roles []*model.Role
if roles, err = getRoles(u); err == nil {
user = model.NewAuthUser(u, roles)
cache.Set(user, cacheKey, session.UserID)
}
return
}
func getRoles(u *model.User) (roles []*model.Role, err error) {
if len(u.Roles) > 0 {
roles = make([]*model.Role, len(u.Roles))
for i, id := range u.Roles {
roles[i], err = biz.Role.Get(id)
if err != nil {
return nil, err
}
}
}
return
}

View File

@ -1,15 +1,49 @@
package misc
package security
import (
"strings"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/model"
)
// Checker check permission of user
func Checker(user web.User, h web.HandlerInfo) bool {
if au, ok := user.(*model.AuthUser); ok {
return au.IsAllowed(h.Name())
}
return false
}
// Permiter is a middleware for validate data permission.
func Permiter(next web.HandlerFunc) web.HandlerFunc {
return func(ctx web.Context) error {
opt := ctx.Handler().Option("perm")
if opt != "" {
array := strings.Split(opt, ",")
err := biz.Perm.Check(ctx.User(), array[0], array[1], ctx.P(array[2]))
if err != nil {
return err
}
}
return next(ctx)
}
}
// Perm holds permission key and description.
type Perm struct {
Key string
Text string
}
// PermGroup holds information of a perm group.
type PermGroup struct {
Name string
Perms []Perm
}
// Perms holds all valid perm groups.
var Perms = []PermGroup{
{
Name: "Registry",