1
0
Fork 0
forked from External/grumble

grumble, pkg/acl: move ACL handling to the acl package.

This commit is contained in:
Mikkel Krautz 2013-02-09 21:33:32 +01:00
parent 3dc3b25f57
commit 3ef203a83f
12 changed files with 608 additions and 608 deletions

169
pkg/acl/acl.go Normal file
View file

@ -0,0 +1,169 @@
// Copyright (c) 2010-2013 The Grumble Authors
// The use of this source code is goverened by a BSD-style
// license that can be found in the LICENSE-file.
package acl
const (
// Per-channel permissions
NonePermission = 0x0
WritePermission = 0x1
TraversePermission = 0x2
EnterPermission = 0x4
SpeakPermission = 0x8
MuteDeafenPermission = 0x10
MovePermission = 0x20
MakeChannelPermission = 0x40
LinkChannelPermission = 0x80
WhisperPermission = 0x100
TextMessagePermission = 0x200
TempChannelPermission = 0x400
// Root channel only
KickPermission = 0x10000
BanPermission = 0x20000
RegisterPermission = 0x40000
SelfRegisterPermission = 0x80000
// Extra flags
CachedPermission = 0x8000000
AllPermissions = 0xf07ff
)
// Permission represents a permission in Mumble's ACL system.
type Permission uint32
// Check whether the given flags are set on perm
func (perm Permission) isSet(check Permission) bool {
return perm&check == check
}
// IsCached checks whether the ACL has its cache bit set,
// signalling that it was returned from an ACLCache.
func (perm Permission) IsCached() bool {
return perm.isSet(CachedPermission)
}
// Clean returns a Permission that has its cache bit cleared.
func (perm Permission) Clean() Permission {
return perm ^Permission(CachedPermission)
}
// An ACL as defined in an ACL context.
// An ACL can be defined for either a user or a group.
type ACL struct {
// The user id that this ACL applied to. If this
// field is -1, the ACL is a group ACL.
UserId int
// The group that this ACL applies to.
Group string
// The ApplyHere flag determines whether the ACL
// should apply to the current channel.
ApplyHere bool
// The ApplySubs flag determines whethr the ACL
// should apply to subchannels.
ApplySubs bool
// The allowed permission flags.
Allow Permission
// The allowed permission flags. The Deny flags override
// permissions set in Allow.
Deny Permission
}
// IsUserACL returns true if the ACL is defined for a user,
// as opposed to a group.
func (acl *ACL) IsUserACL() bool {
return acl.UserId != -1
}
// IsChannelACL returns true if the ACL is defined for a group,
// as opposed to a user.
func (acl *ACL) IsChannelACL() bool {
return !acl.IsUserACL()
}
// HasPermission checks whether the given user has permission perm in the given context.
// The permission perm must be a single permission and not a combination of permissions.
func HasPermission(ctx *Context, user User, perm Permission) bool {
// We can't check permissions on a nil ctx.
if (ctx == nil) {
panic("acl: HasPermission got nil context")
}
// SuperUser can't speak or whisper, but everything else is OK
if user.UserId() == 0 {
if perm == SpeakPermission || perm == WhisperPermission {
return false
}
return true
}
// Default permissions
defaults := Permission(TraversePermission | EnterPermission | SpeakPermission | WhisperPermission | TextMessagePermission)
granted := defaults
contexts := buildChain(ctx)
origCtx := ctx
traverse := true
write := false
for _, ctx := range contexts {
// If the context does not inherit any ACLs, use the default permissions.
if !ctx.InheritACL {
granted = defaults
}
// Iterate through ACLs that are defined on ctx. Note: this does not include
// ACLs that iter has inherited from a parent (unless there is also a group on
// iter with the same name, that changes the permissions a bit!)
for _, acl := range ctx.ACLs {
// Determine whether the ACL applies to user.
// If it is a user ACL and the user id of the ACL
// matches user's id, we're good to go.
//
// If it's a group ACL, we have to parse and interpret
// the group string in the current context to determine
// membership. For that we use GroupMemberCheck.
matchUser := acl.IsUserACL() && acl.UserId == user.UserId()
matchGroup := GroupMemberCheck(origCtx, ctx, acl.Group, user)
if matchUser || matchGroup {
if acl.Allow.isSet(TraversePermission) {
traverse = true
}
if acl.Deny.isSet(TraversePermission) {
traverse = false
}
if acl.Allow.isSet(WritePermission) {
write = true
}
if acl.Deny.isSet(WritePermission) {
write = false
}
if (origCtx == ctx && acl.ApplyHere) || (origCtx != ctx && acl.ApplySubs) {
granted |= acl.Allow
granted &= ^acl.Deny
}
}
}
// If traverse is not set and the user doesn't have write permissions
// on the channel, the user will not have any permissions.
// This is because -traverse removes all permissions, and +write grants
// all permissions.
if !traverse && !write {
granted = NonePermission
break
}
}
// The +write permission implies all permissions except for +speak and +whisper.
// This means that if the user has WritePermission, we should return true for all
// permissions exccept SpeakPermission and WhisperPermission.
if perm != SpeakPermission && perm != WhisperPermission {
return (granted & (perm | WritePermission)) != NonePermission
} else {
return (granted & perm) != NonePermission
}
return false
}

48
pkg/acl/context.go Normal file
View file

@ -0,0 +1,48 @@
// Copyright (c) 2010-2013 The Grumble Authors
// The use of this source code is goverened by a BSD-style
// license that can be found in the LICENSE-file.
package acl
// Context represents a context in which ACLs can
// be understood. Typically embedded into a type
// that represents a Mumble channel.
type Context struct {
// Parent points to the context's parent.
// May be nil if the Context does not have a parent.
Parent *Context
// ACLs is the Context's list of ACL entries.
ACLs []ACL
// Groups is the Context's representation of groups.
// It is indexed by the Group's name.
Groups map[string]Group
// InheritACL determines whether this context should
// inherit ACLs from its parent.
InheritACL bool
}
// indexOf finds the index of the context ctx in the context chain contexts.
// Returns -1 if the given context was not found in the context chain.
func indexOf(contexts []*Context, ctx *Context) int {
for i, iter := range contexts {
if iter == ctx {
return i
}
}
return -1
}
// buildChain walks from the context ctx back through all of its parents,
// collecting them all in a slice. The first element of the returned
// slice is the final ancestor (it has a nil Parent).
func buildChain(ctx *Context) []*Context {
chain := []*Context{}
for ctx != nil {
chain = append([]*Context{ctx}, chain...)
ctx = ctx.Parent
}
return chain
}

370
pkg/acl/group.go Normal file
View file

@ -0,0 +1,370 @@
// Copyright (c) 2010-2013 The Grumble Authors
// The use of this source code is goverened by a BSD-style
// license that can be found in the LICENSE-file.
package acl
import (
"log"
"strconv"
"strings"
)
// Group represents a Group in an Context.
type Group struct {
// The name of this group
Name string
// The inherit flag means that this group will inherit group
// members from its parent.
Inherit bool
// The inheritable flag means that subchannels can
// inherit the members of this group.
Inheritable bool
// Group adds permissions to these users
Add map[int]bool
// Group removes permissions from these users
Remove map[int]bool
// Temporary add (authenticators)
Temporary map[int]bool
}
// EmptyGroupWithName creates a new Group with the given name.
func EmptyGroupWithName(name string) Group {
grp := Group{}
grp.Name = name
grp.Add = make(map[int]bool)
grp.Remove = make(map[int]bool)
grp.Temporary = make(map[int]bool)
return grp
}
// AddContains checks whether the Add set contains id.
func (group *Group) AddContains(id int) (ok bool) {
_, ok = group.Add[id]
return
}
// AddUsers gets the list of user ids in the Add set.
func (group *Group) AddUsers() []int {
users := []int{}
for uid, _ := range group.Add {
users = append(users, uid)
}
return users
}
// RemoveContains checks whether the Remove set contains id.
func (group *Group) RemoveContains(id int) (ok bool) {
_, ok = group.Remove[id]
return
}
// RemoveUsers gets the list of user ids in the Remove set.
func (group *Group) RemoveUsers() []int {
users := []int{}
for uid, _ := range group.Remove {
users = append(users, uid)
}
return users
}
// TemporaryContains checks whether the Temporary set contains id.
func (group *Group) TemporaryContains(id int) (ok bool) {
_, ok = group.Temporary[id]
return
}
// MembersInContext gets the set of user id's from the group in the given context.
// This includes group members that have been inherited from an ancestor context.
func (group *Group) MembersInContext(ctx *Context) map[int]bool {
groups := []Group{}
members := map[int]bool{}
// Walk a group's context chain, starting with the context the group
// is defined on, followed by its parent contexts.
origCtx := ctx
for ctx != nil {
curgroup, ok := ctx.Groups[group.Name]
if ok {
// If the group is not inheritable, and we're looking at an
// ancestor group, we've looked in all the groups we should.
if ctx != origCtx && !curgroup.Inheritable {
break
}
// Add the group to the list of groups to be considered
groups = append([]Group{curgroup}, groups...)
// If this group does not inherit from groups in its ancestors, stop looking
// for more ancestor groups.
if !curgroup.Inherit {
break
}
}
ctx = ctx.Parent
}
for _, curgroup := range groups {
for uid, _ := range curgroup.Add {
members[uid] = true
}
for uid, _ := range curgroup.Remove {
delete(members, uid)
}
}
return members
}
// GroupMemberCheck checks whether a user is a member
// of the group as defined in the given context.
//
// The 'current' context is the context that group
// membership is currently being evaluated for.
//
// The 'acl' context is the context of the ACL that
// that group membership is being evaluated for.
//
// The acl context will always be either equal to
// current, or be an ancestor.
func GroupMemberCheck(current *Context, acl *Context, name string, user User) (ok bool) {
valid := true
invert := false
token := false
hash := false
// Returns the 'correct' return value considering the value
// of the invert flag.
defer func() {
if valid && invert {
ok = !ok
}
}()
channel := current
for {
// Empty group name are not valid.
if len(name) == 0 {
valid = false
return false
}
// Invert
if name[0] == '!' {
invert = true
name = name[1:]
continue
}
// Evaluate in ACL context (not current channel)
if name[0] == '~' {
channel = acl
name = name[1:]
continue
}
// Token
if name[0] == '#' {
token = true
name = name[1:]
continue
}
// Hash
if name[0] == '$' {
hash = true
name = name[1:]
continue
}
break
}
if token {
// The user is part of this group if the remaining name is part of
// his access token list. The name check is case-insensitive.
for _, token := range user.Tokens() {
if strings.ToLower(name) == strings.ToLower(token) {
return true
}
}
return false
} else if hash {
// The client is part of this group if the remaining name matches the
// client's cert hash.
if strings.ToLower(name) == strings.ToLower(user.CertHash()) {
return true
}
return false
} else if name == "none" {
// None
return false
} else if name == "all" {
// Everyone
return true
} else if name == "auth" {
// The user is part of the auth group is he is authenticated. That is,
// his UserId is >= 0.
return user.UserId() >= 0
} else if name == "strong" {
// The user is part of the strong group if he is authenticated to the server
// via a strong certificate (i.e. non-self-signed, trusted by the server's
// trusted set of root CAs).
log.Printf("GroupMemberCheck: Implement strong certificate matching")
return false
} else if name == "in" {
// Is the user in the currently evaluated channel?
return user.ACLContext() == channel
} else if name == "out" {
// Is the user not in the currently evaluated channel?
return user.ACLContext() != channel
} else if name == "sub" {
// fixme(mkrautz): The sub group implementation below hasn't been thoroughly
// tested yet. It might be a bit buggy!
// Strip away the "sub," part of the name
name = name[4:]
mindesc := 1
maxdesc := 1000
minpath := 0
// Parse the groupname to extract the values we should use
// for minpath (first argument), mindesc (second argument),
// and maxdesc (third argument).
args := strings.SplitN(name, ",", 3)
nargs := len(args)
if nargs == 3 {
if len(args[2]) > 0 {
if result, err := strconv.Atoi(args[2]); err == nil {
maxdesc = result
}
}
}
if nargs >= 2 {
if len(args[1]) > 0 {
if result, err := strconv.Atoi(args[1]); err == nil {
mindesc = result
}
}
}
if nargs >= 1 {
if len(args[0]) > 0 {
if result, err := strconv.Atoi(args[0]); err == nil {
minpath = result
}
}
}
// Build a context chain starting from the
// user's current context.
userChain := buildChain(user.ACLContext())
// Build a chain of contexts, starting from
// the 'current' context. This is the context
// that group membership is checked against,
// notwithstanding the ~ group operator.
groupChain := buildChain(current)
// Find the index of the context that the group
// is currently being evaluated on. This can be
// either the 'acl' context or 'current' context
// depending on the ~ group operator.
cofs := indexOf(groupChain, current)
if cofs == -1 {
valid = false
return false
}
// Add the first parameter of our sub group to cofs
// to get our base context.
cofs += minpath
// Check that the minpath parameter that was given
// is a valid index for groupChain.
if cofs >= len(groupChain) {
valid = false
return false
} else if cofs < 0 {
cofs = 0
}
// If our base context is not in the userChain, the
// group does not apply to the user.
if indexOf(userChain, groupChain[cofs]) == -1 {
return false
}
// Down here, we're certain that the userChain
// includes the base context somewhere in its
// chain. We must now determine if the path depth
// makes the user a member of the group.
mindepth := cofs + mindesc
maxdepth := cofs + maxdesc
pdepth := len(userChain) - 1
return pdepth >= mindepth && pdepth <= maxdepth
} else {
// Non-magic groups
groups := []Group{}
iter := channel
for iter != nil {
if group, ok := iter.Groups[name]; ok {
// Skip non-inheritable groups if we're in parents
// of our evaluated context.
if iter != channel && !group.Inheritable {
break
}
// Prepend group
groups = append([]Group{group}, groups...)
// If this group does not inherit from groups in its ancestors, stop looking
// for more ancestor groups.
if !group.Inherit {
break
}
}
iter = iter.Parent
}
isMember := false
for _, group := range groups {
if group.AddContains(user.UserId()) || group.TemporaryContains(user.UserId()) || group.TemporaryContains(-int(user.Session())) {
isMember = true
}
if group.RemoveContains(user.UserId()) {
isMember = false
}
}
return isMember
}
return false
}
// Get the list of group names for the given ACL context.
//
// This function walks the through the context chain to figure
// out all groups that affect the given context whilst considering
// group inheritance.
func (ctx *Context) GroupNames() []string {
names := map[string]bool{}
origCtx := ctx
contexts := []*Context{}
// Walk through the whole context chain and all groups in it.
for _, ctx := range contexts {
for _, group := range ctx.Groups {
// A non-inheritable group in parent. Discard it.
if ctx != origCtx && !group.Inheritable {
delete(names, group.Name)
// An inheritable group. Add it to the list.
} else {
names[group.Name] = true
}
}
}
// Convert to slice
stringNames := make([]string, 0, len(names))
for name, ok := range names {
if ok {
stringNames = append(stringNames, name)
}
}
return stringNames
}

23
pkg/acl/interfaces.go Normal file
View file

@ -0,0 +1,23 @@
// Copyright (c) 2013 The Grumble Authors
// The use of this source code is goverened by a BSD-style
// license that can be found in the LICENSE-file.
package acl
// User represents a user on a Mumble server.
// The User interface represents the method set that
// must be implemented in order to check a user's
// permissions in an ACL context.
type User interface {
Session() uint32
UserId() int
CertHash() string
Tokens() []string
ACLContext() *Context
}
// Channel represents a Channel on a Mumble server.
type Channel interface {
ChannelId() int
}