617 lines
12 KiB
Go
617 lines
12 KiB
Go
package mercury
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
type Config []*Space
|
|
|
|
func NewConfig(spaces ...*Space) Config {
|
|
return spaces
|
|
}
|
|
func (c *Config) AddSpace(spaces ...*Space) *Config {
|
|
*c = append(*c, spaces...)
|
|
return c
|
|
}
|
|
|
|
// Len implements Len for sort.interface
|
|
func (lis Config) Len() int {
|
|
return len(lis)
|
|
}
|
|
|
|
// Less implements Less for sort.interface
|
|
func (lis Config) Less(i, j int) bool {
|
|
return lis[i].Space < lis[j].Space
|
|
}
|
|
|
|
// Swap implements Swap for sort.interface
|
|
func (lis Config) Swap(i, j int) { lis[i], lis[j] = lis[j], lis[i] }
|
|
|
|
// StringList returns the space names as a list
|
|
func (lis Config) StringList() string {
|
|
var buf strings.Builder
|
|
for _, o := range lis {
|
|
if len(o.Notes) > 0 {
|
|
buf.WriteString("# ")
|
|
buf.WriteString(strings.Join(o.Notes, "\n# "))
|
|
buf.WriteRune('\n')
|
|
}
|
|
buf.WriteRune('@')
|
|
buf.WriteString(o.Space)
|
|
if len(o.Tags) > 0 {
|
|
buf.WriteRune(' ')
|
|
buf.WriteString(strings.Join(o.Tags, " "))
|
|
}
|
|
buf.WriteRune('\n')
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// ToSpaceMap formats as SpaceMap
|
|
func (lis Config) ToSpaceMap() SpaceMap {
|
|
out := make(SpaceMap)
|
|
for _, c := range lis {
|
|
out[c.Space] = c
|
|
}
|
|
return out
|
|
}
|
|
|
|
// String format config as string
|
|
func (lis Config) String() string {
|
|
attLen := 0
|
|
tagLen := 0
|
|
|
|
for _, o := range lis {
|
|
for _, v := range o.List {
|
|
l := len(v.Name)
|
|
if attLen <= l {
|
|
attLen = l
|
|
}
|
|
|
|
t := len(strings.Join(v.Tags, " "))
|
|
if tagLen <= t {
|
|
tagLen = t
|
|
}
|
|
}
|
|
}
|
|
|
|
var buf strings.Builder
|
|
for _, o := range lis {
|
|
if len(o.Notes) > 0 {
|
|
buf.WriteString("# ")
|
|
buf.WriteString(strings.Join(o.Notes, "\n# "))
|
|
buf.WriteRune('\n')
|
|
}
|
|
|
|
buf.WriteRune('@')
|
|
buf.WriteString(o.Space)
|
|
if len(o.Tags) > 0 {
|
|
buf.WriteRune(' ')
|
|
buf.WriteString(strings.Join(o.Tags, " "))
|
|
}
|
|
buf.WriteRune('\n')
|
|
|
|
for _, v := range o.List {
|
|
if len(v.Notes) > 0 {
|
|
buf.WriteString("# ")
|
|
buf.WriteString(strings.Join(v.Notes, "\n# "))
|
|
buf.WriteString("\n")
|
|
}
|
|
|
|
buf.WriteString(v.Name)
|
|
buf.WriteString(strings.Repeat(" ", attLen-len(v.Name)+1))
|
|
|
|
if len(v.Tags) > 0 {
|
|
t := strings.Join(v.Tags, " ")
|
|
buf.WriteString(t)
|
|
buf.WriteString(strings.Repeat(" ", tagLen-len(t)+1))
|
|
} else {
|
|
buf.WriteString(strings.Repeat(" ", tagLen+1))
|
|
}
|
|
|
|
switch len(v.Values) {
|
|
case 0:
|
|
buf.WriteString("\n")
|
|
case 1:
|
|
buf.WriteString(" :")
|
|
buf.WriteString(v.Values[0])
|
|
buf.WriteString("\n")
|
|
default:
|
|
buf.WriteString(" :")
|
|
buf.WriteString(v.Values[0])
|
|
buf.WriteString("\n")
|
|
for _, s := range v.Values[1:] {
|
|
buf.WriteString(strings.Repeat(" ", attLen+tagLen+3))
|
|
buf.WriteString(":")
|
|
buf.WriteString(s)
|
|
buf.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
buf.WriteRune('\n')
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// EnvString format config as environ
|
|
func (lis Config) EnvString() string {
|
|
var buf strings.Builder
|
|
for _, o := range lis {
|
|
for _, v := range o.List {
|
|
buf.WriteString(o.Space)
|
|
for _, t := range o.Tags {
|
|
buf.WriteRune(' ')
|
|
buf.WriteString(t)
|
|
}
|
|
buf.WriteRune(':')
|
|
buf.WriteString(v.Name)
|
|
for _, t := range v.Tags {
|
|
buf.WriteRune(' ')
|
|
buf.WriteString(t)
|
|
}
|
|
switch len(v.Values) {
|
|
case 0:
|
|
buf.WriteRune('=')
|
|
buf.WriteRune('\n')
|
|
case 1:
|
|
buf.WriteRune('=')
|
|
buf.WriteString(v.Values[0])
|
|
buf.WriteRune('\n')
|
|
default:
|
|
buf.WriteRune('+')
|
|
buf.WriteRune('=')
|
|
buf.WriteString(v.Values[0])
|
|
buf.WriteRune('\n')
|
|
for _, s := range v.Values[1:] {
|
|
buf.WriteString(o.Space)
|
|
buf.WriteRune(':')
|
|
buf.WriteString(v.Name)
|
|
buf.WriteRune('+')
|
|
buf.WriteRune('=')
|
|
buf.WriteString(s)
|
|
buf.WriteRune('\n')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// IniString format config as ini
|
|
func (lis Config) IniString() string {
|
|
var buf strings.Builder
|
|
for _, o := range lis {
|
|
buf.WriteRune('[')
|
|
buf.WriteString(o.Space)
|
|
buf.WriteRune(']')
|
|
buf.WriteRune('\n')
|
|
for _, v := range o.List {
|
|
buf.WriteString(v.Name)
|
|
switch len(v.Values) {
|
|
case 0:
|
|
buf.WriteRune('=')
|
|
buf.WriteRune('\n')
|
|
case 1:
|
|
buf.WriteRune('=')
|
|
buf.WriteString(v.Values[0])
|
|
buf.WriteRune('\n')
|
|
default:
|
|
buf.WriteRune('[')
|
|
buf.WriteRune('0')
|
|
buf.WriteRune(']')
|
|
|
|
buf.WriteRune('=')
|
|
buf.WriteString(v.Values[0])
|
|
buf.WriteRune('\n')
|
|
for i, s := range v.Values[1:] {
|
|
buf.WriteString(v.Name)
|
|
buf.WriteRune('[')
|
|
buf.WriteString(fmt.Sprintf("%d", i))
|
|
buf.WriteRune(']')
|
|
buf.WriteRune('=')
|
|
buf.WriteString(s)
|
|
buf.WriteRune('\n')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// Space stores a registry of spaces
|
|
type Space struct {
|
|
Space string `json:"space"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
Notes []string `json:"notes,omitempty"`
|
|
List []Value `json:"list,omitempty"`
|
|
}
|
|
|
|
func NewSpace(space string) *Space {
|
|
return &Space{Space: space}
|
|
}
|
|
|
|
// HasTag returns true if needle is found
|
|
// If the needle ends with a / it will be treated
|
|
// as a prefix for tag meta data.
|
|
func (s *Space) HasTag(needle string) bool {
|
|
isPrefix := strings.HasSuffix(needle, "/")
|
|
for i := range s.Tags {
|
|
switch isPrefix {
|
|
case true:
|
|
if strings.HasPrefix(s.Tags[i], needle) {
|
|
return true
|
|
}
|
|
case false:
|
|
if s.Tags[i] == needle {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetTagMeta retuns the value after a '/' in a tag.
|
|
// Tags are in the format 'name' or 'name/value'
|
|
// This function returns the value.
|
|
func (s *Space) GetTagMeta(needle string, offset int) string {
|
|
if !strings.HasSuffix(needle, "/") {
|
|
needle += "/"
|
|
}
|
|
|
|
for i := range s.Tags {
|
|
if strings.HasPrefix(s.Tags[i], needle) {
|
|
if offset > 0 {
|
|
offset--
|
|
continue
|
|
}
|
|
|
|
return strings.TrimPrefix(s.Tags[i], needle)
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// FirstTagMeta returns the first meta tag value.
|
|
func (s *Space) FirstTagMeta(needle string) string {
|
|
return s.GetTagMeta(needle, 0)
|
|
}
|
|
|
|
// GetValues that match name
|
|
func (s *Space) GetValues(name string) (lis []Value) {
|
|
for i := range s.List {
|
|
if s.List[i].Name == name {
|
|
lis = append(lis, s.List[i])
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// FirstValue that matches name
|
|
func (s *Space) FirstValue(name string) Value {
|
|
for i := range s.List {
|
|
if s.List[i].Name == name {
|
|
return s.List[i]
|
|
}
|
|
}
|
|
return Value{}
|
|
}
|
|
|
|
func (s *Space) SetTags(tags ...string) *Space {
|
|
s.Tags = tags
|
|
return s
|
|
}
|
|
func (s *Space) AddTags(tags ...string) *Space {
|
|
s.Tags = append(s.Tags, tags...)
|
|
return s
|
|
}
|
|
func (s *Space) SetNotes(notes ...string) *Space {
|
|
s.Notes = notes
|
|
return s
|
|
}
|
|
func (s *Space) AddNotes(notes ...string) *Space {
|
|
s.Notes = append(s.Notes, notes...)
|
|
return s
|
|
}
|
|
func (s *Space) SetKeys(keys ...*Value) *Space {
|
|
for i := range keys {
|
|
k := *keys[i]
|
|
k.Seq = uint64(i)
|
|
s.List = append(s.List, k)
|
|
}
|
|
|
|
return s
|
|
}
|
|
func (s *Space) AddKeys(keys ...*Value) *Space {
|
|
l := uint64(len(s.List))
|
|
for i := range keys {
|
|
k := *keys[i]
|
|
k.Seq = uint64(i) + l
|
|
s.List = append(s.List, k)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// SpaceMap generic map of space values
|
|
type SpaceMap map[string]*Space
|
|
|
|
// Rule is a type of rule
|
|
type Rule struct {
|
|
Role string
|
|
Type string
|
|
Match string
|
|
}
|
|
|
|
// Rules is a list of rules
|
|
type Rules []Rule
|
|
|
|
// GetNamespaceSearch returns a default search for users rules.
|
|
func (r Rules) GetNamespaceSearch() (lis NamespaceSearch) {
|
|
for _, o := range r {
|
|
if o.Type == "NS" && (o.Role == "read" || o.Role == "write") {
|
|
lis = append(lis, NamespaceStar(o.Match))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check if name matches rule
|
|
func (r Rule) Check(name string) bool {
|
|
ok, err := filepath.Match(r.Match, name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return ok
|
|
}
|
|
|
|
// CheckNamespace verifies user has access
|
|
func (r Rules) CheckNamespace(search NamespaceSearch) bool {
|
|
for _, ns := range search {
|
|
if !r.GetRoles("NS", ns.Value()).HasRole("read", "write") {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (r Rules) Less(i, j int) bool {
|
|
si, sj := scoreRule(r[i]), scoreRule(r[j])
|
|
if si != sj {
|
|
return si < sj
|
|
}
|
|
return len(r[i].Match) < len(r[j].Match)
|
|
}
|
|
func (r Rules) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
|
func (r Rules) Len() int { return len(r) }
|
|
|
|
func scoreRule(r Rule) int {
|
|
score := 0
|
|
if r.Type == "GR" {
|
|
score += 1000
|
|
}
|
|
switch r.Role {
|
|
case "admin":
|
|
score += 100
|
|
case "write":
|
|
score += 50
|
|
case "read":
|
|
score += 10
|
|
}
|
|
return score
|
|
}
|
|
|
|
// ReduceSearch verifies user has access
|
|
func (r Rules) ReduceSearch(search NamespaceSearch) (out NamespaceSearch) {
|
|
rules := r.GetNamespaceSearch()
|
|
skip := make(map[string]struct{})
|
|
out = make(NamespaceSearch, 0, len(rules))
|
|
|
|
for _, rule := range rules {
|
|
if _, ok := skip[rule.Raw()]; ok {
|
|
continue
|
|
}
|
|
for _, ck := range search {
|
|
if _, ok := skip[ck.Raw()]; ok {
|
|
continue
|
|
} else if rule.Match(ck.Raw()) {
|
|
skip[ck.Raw()] = struct{}{}
|
|
out = append(out, ck)
|
|
} else if ck.Match(rule.Raw()) {
|
|
out = append(out, rule)
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Roles is a list of roles for a resource
|
|
type Roles map[string]struct{}
|
|
|
|
// GetRoles returns a list of Roles
|
|
func (r Rules) GetRoles(typ, name string) (lis Roles) {
|
|
lis = make(Roles)
|
|
for _, o := range r {
|
|
if typ == o.Type && o.Check(name) {
|
|
lis[o.Role] = struct{}{}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// HasRole is a valid role
|
|
func (r Roles) HasRole(roles ...string) bool {
|
|
for _, role := range roles {
|
|
if _, ok := r[role]; ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// func (rules Rules) filterSpace(lis Config) (out Config, err error) {
|
|
// accessList := make(map[string]struct{})
|
|
// for _, o := range lis {
|
|
// if _, ok := accessList[o.Space]; ok {
|
|
// out = append(out, o)
|
|
// continue
|
|
// }
|
|
|
|
// if role := rules.GetRoles("NS", o.Space); role.HasRole("read", "write") && !role.HasRole("deny") {
|
|
// accessList[o.Space] = struct{}{}
|
|
// out = append(out, o)
|
|
// }
|
|
// }
|
|
|
|
// return
|
|
// }
|
|
|
|
// ToArray converts SpaceMap to ArraySpace
|
|
func (m SpaceMap) ToArray() Config {
|
|
a := make(Config, 0, len(m))
|
|
for _, s := range m {
|
|
a = append(a, s)
|
|
}
|
|
return a
|
|
}
|
|
func (m *SpaceMap) MergeMap(s SpaceMap) {
|
|
m.Merge(maps.Values(s)...)
|
|
}
|
|
func (m *SpaceMap) Merge(lis ...*Space) {
|
|
for _, s := range lis {
|
|
// Only accept first version based on priority.
|
|
if _, ok := (*m)[s.Space]; ok {
|
|
continue
|
|
}
|
|
|
|
(*m)[s.Space] = s
|
|
|
|
// // Merge values together.
|
|
// c, ok := (*m)[s.Space]
|
|
// if ok {
|
|
// c = &Space{}
|
|
// }
|
|
// c.Notes = append(c.Notes, s.Notes...)
|
|
// c.Tags = append(c.Tags, s.Tags...)
|
|
// last := c.List[len(c.List)-1].Seq
|
|
// for i := range s.List {
|
|
// v := s.List[i]
|
|
// v.Seq += last
|
|
// c.List = append(c.List, v)
|
|
// }
|
|
// (*m)[s.Space] = c
|
|
}
|
|
}
|
|
|
|
// Value stores the attributes for space values
|
|
type Value struct {
|
|
Space string `json:"-" db:"space"`
|
|
Seq uint64 `json:"-" db:"seq"`
|
|
Name string `json:"name"`
|
|
Values []string `json:"values"`
|
|
Notes []string `json:"notes"`
|
|
Tags []string `json:"tags"`
|
|
}
|
|
|
|
// func (v *Value) ID() string {
|
|
// return gql.FmtID("MercurySpace:%v:%v", v.Space, v.Seq)
|
|
// }
|
|
|
|
// HasTag returns true if needle is found
|
|
// If the needle ends with a / it will be treated
|
|
// as a prefix for tag meta data.
|
|
func (v Value) HasTag(needle string) bool {
|
|
isPrefix := strings.HasSuffix(needle, "/")
|
|
for i := range v.Tags {
|
|
switch isPrefix {
|
|
case true:
|
|
if strings.HasPrefix(v.Tags[i], needle) {
|
|
return true
|
|
}
|
|
case false:
|
|
if v.Tags[i] == needle {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetTagMeta retuns the value after a '/' in a tag.
|
|
// Tags are in the format 'name' or 'name/value'
|
|
// This function returns the value.
|
|
func (v Value) GetTagMeta(needle string, offset int) string {
|
|
if !strings.HasSuffix(needle, "/") {
|
|
needle += "/"
|
|
}
|
|
|
|
for i := range v.Tags {
|
|
if strings.HasPrefix(v.Tags[i], needle) {
|
|
if offset > 0 {
|
|
offset--
|
|
continue
|
|
}
|
|
|
|
return strings.TrimPrefix(v.Tags[i], needle)
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// FirstTagMeta returns the first meta tag value.
|
|
func (v Value) FirstTagMeta(needle string) string {
|
|
return v.GetTagMeta(needle, 0)
|
|
}
|
|
|
|
// First value in array.
|
|
func (v Value) First() string {
|
|
if len(v.Values) < 1 {
|
|
return ""
|
|
}
|
|
|
|
return v.Values[0]
|
|
}
|
|
|
|
// Join values with newlines.
|
|
func (v Value) Join() string {
|
|
return strings.Join(v.Values, "\n")
|
|
}
|
|
|
|
func NewValue(name string) *Value {
|
|
return &Value{Name: name}
|
|
}
|
|
func (v *Value) SetTags(tags ...string) *Value {
|
|
v.Tags = tags
|
|
return v
|
|
}
|
|
func (v *Value) AddTags(tags ...string) *Value {
|
|
v.Tags = append(v.Tags, tags...)
|
|
return v
|
|
}
|
|
func (v *Value) SetNotes(notes ...string) *Value {
|
|
v.Notes = notes
|
|
return v
|
|
}
|
|
func (v *Value) AddNotes(notes ...string) *Value {
|
|
v.Notes = append(v.Notes, notes...)
|
|
return v
|
|
}
|
|
func (v *Value) SetValues(values ...string) *Value {
|
|
v.Values = values
|
|
return v
|
|
}
|
|
func (v *Value) AddValues(values ...string) *Value {
|
|
v.Values = append(v.Values, values...)
|
|
return v
|
|
}
|