Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions eos/fetch_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package eos

import (
"context"
"fmt"
"strings"
)

func (c *Client) AccessList(ctx context.Context) ([]AccessRecord, error) {
_ = ctx

output, err := c.runCommand("eos", "access", "ls", "-m")
if err != nil {
return nil, fmt.Errorf("eos access ls -m: %w", err)
}

return parseAccessList(output), nil
}

func (c *Client) SetAccessRule(ctx context.Context, op, category, value string) error {
_ = ctx

args, err := accessRuleArgs(op, category, value)
if err != nil {
return err
}
if _, err := c.runCommand(args...); err != nil {
return fmt.Errorf("%s %s %s: %w", strings.Join(args[:3], " "), category, value, err)
}
return nil
}

func (c *Client) SetAccessStall(ctx context.Context, seconds int) error {
_ = ctx

args, err := accessStallArgs(seconds)
if err != nil {
return err
}
if _, err := c.runCommand(args...); err != nil {
return fmt.Errorf("%s: %w", strings.Join(args, " "), err)
}
return nil
}

func parseAccessList(output []byte) []AccessRecord {
lines := strings.Split(string(output), "\n")
records := make([]AccessRecord, 0, len(lines))
for _, rawLine := range lines {
line := strings.TrimSpace(rawLine)
if line == "" {
continue
}

rawKey, value, found := strings.Cut(line, "=")
if !found {
continue
}

rawKey = strings.TrimSpace(rawKey)
value = strings.TrimSpace(value)
category, rule, hasRule := strings.Cut(rawKey, ".")
if !hasRule {
category = rawKey
rule = "value"
}

records = append(records, AccessRecord{
Category: category,
Rule: rule,
Value: value,
RawKey: rawKey,
})
}

return records
}

func accessRuleArgs(op, category, value string) ([]string, error) {
op = strings.TrimSpace(strings.ToLower(op))
category = strings.TrimSpace(strings.ToLower(category))
value = strings.TrimSpace(value)

switch op {
case "allow", "unallow", "ban", "unban":
default:
return nil, fmt.Errorf("unsupported access action %q", op)
}

switch category {
case "user", "group", "host", "domain":
default:
return nil, fmt.Errorf("unsupported access category %q", category)
}

if value == "" {
return nil, fmt.Errorf("access value is required")
}

return []string{"eos", "access", op, category, value}, nil
}

func accessStallArgs(seconds int) ([]string, error) {
if seconds <= 0 {
return nil, fmt.Errorf("stall seconds must be positive")
}
return []string{"eos", "access", "set", "stall", fmt.Sprintf("%d", seconds)}, nil
}
63 changes: 63 additions & 0 deletions eos/fetch_access_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package eos

import "testing"

func TestParseAccessList(t *testing.T) {
output := []byte(`
user.banned=eosnobody
user.allowed=lobisapa
group.allowed=root
redirect=host-a:1094
`)

got := parseAccessList(output)
want := []AccessRecord{
{Category: "user", Rule: "banned", Value: "eosnobody", RawKey: "user.banned"},
{Category: "user", Rule: "allowed", Value: "lobisapa", RawKey: "user.allowed"},
{Category: "group", Rule: "allowed", Value: "root", RawKey: "group.allowed"},
{Category: "redirect", Rule: "value", Value: "host-a:1094", RawKey: "redirect"},
}

if len(got) != len(want) {
t.Fatalf("expected %d records, got %d: %+v", len(want), len(got), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("record %d: got %+v want %+v", i, got[i], want[i])
}
}
}

func TestAccessRuleArgs(t *testing.T) {
got, err := accessRuleArgs("ban", "user", "lobisapa")
if err != nil {
t.Fatalf("accessRuleArgs returned error: %v", err)
}

want := []string{"eos", "access", "ban", "user", "lobisapa"}
if len(got) != len(want) {
t.Fatalf("arg count = %d, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("arg %d = %q, want %q", i, got[i], want[i])
}
}
}

func TestAccessStallArgs(t *testing.T) {
got, err := accessStallArgs(300)
if err != nil {
t.Fatalf("accessStallArgs returned error: %v", err)
}

want := []string{"eos", "access", "set", "stall", "300"}
if len(got) != len(want) {
t.Fatalf("arg count = %d, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("arg %d = %q, want %q", i, got[i], want[i])
}
}
}
13 changes: 12 additions & 1 deletion eos/fetch_inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ func (c *Client) Inspector(ctx context.Context) (InspectorStats, error) {

output, err := c.runCommand("eos", "inspector", "-l", "-m")
if err != nil {
return InspectorStats{}, fmt.Errorf("eos inspector -l -m: %w", err)
msg := strings.TrimSpace(string(output))
lower := strings.ToLower(msg)
switch {
case strings.Contains(lower, "permission denied"):
return InspectorStats{}, fmt.Errorf("inspector unavailable on this host: permission denied")
case strings.Contains(lower, "inspector disabled"):
return InspectorStats{}, fmt.Errorf("inspector disabled")
case msg != "":
return InspectorStats{}, fmt.Errorf("eos inspector -l -m: %s", msg)
default:
return InspectorStats{}, fmt.Errorf("eos inspector -l -m: %w", err)
}
}

return parseInspectorStats(output), nil
Expand Down
7 changes: 7 additions & 0 deletions eos/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ type VIDRecord struct {
Value string
}

type AccessRecord struct {
Category string
Rule string
Value string
RawKey string
}

type InspectorLayoutSummary struct {
Layout string
Type string
Expand Down
29 changes: 29 additions & 0 deletions ui/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,35 @@ func loadVIDCmd(client *eos.Client, mode vidListMode) tea.Cmd {
}
}

func loadAccessCmd(client *eos.Client) tea.Cmd {
return func() tea.Msg {
records, err := client.AccessList(context.Background())
return accessLoadedMsg{records: records, err: err}
}
}

func runAccessRuleCmd(client *eos.Client, op, category, value string) tea.Cmd {
return func() tea.Msg {
err := client.SetAccessRule(context.Background(), op, category, value)
return accessActionResultMsg{
op: op,
target: fmt.Sprintf("%s %s %s", op, category, value),
err: err,
}
}
}

func runAccessStallCmd(client *eos.Client, seconds int) tea.Cmd {
return func() tea.Msg {
err := client.SetAccessStall(context.Background(), seconds)
return accessActionResultMsg{
op: "stall",
target: fmt.Sprintf("set stall %ds", seconds),
err: err,
}
}
}

func loadNamespaceStatsCmd(client *eos.Client) tea.Cmd {
return func() tea.Msg {
stats, err := client.NamespaceStats(context.Background())
Expand Down
94 changes: 94 additions & 0 deletions ui/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ func (m model) visibleGroups() []eos.GroupRecord {
return groups
}

func (m model) visibleAccessRecords() []eos.AccessRecord {
records := append([]eos.AccessRecord(nil), m.accessRecords...)
if len(m.accessFilter.filters) == 0 {
return records
}

filtered := make([]eos.AccessRecord, 0, len(records))
for _, record := range records {
if m.matchesAccessFilters(record) {
filtered = append(filtered, record)
}
}
return filtered
}

func (m model) visibleSpaces() []eos.SpaceRecord {
spaces := append([]eos.SpaceRecord(nil), m.spaces...)
if len(m.spaceFilter.filters) > 0 {
Expand Down Expand Up @@ -222,6 +237,30 @@ func (m model) matchesNamespaceFilters(entry eos.Entry) bool {
return true
}

func (m model) matchesAccessFilters(record eos.AccessRecord) bool {
for col, filter := range m.accessFilter.filters {
if filter == "" {
continue
}
if !matchesFilterQuery(m.accessFilterValueForColumn(record, col), filter) {
return false
}
}
return true
}

func (m model) matchesAccessFiltersExcept(record eos.AccessRecord, excludeColumn int) bool {
for col, filter := range m.accessFilter.filters {
if col == excludeColumn || filter == "" {
continue
}
if !matchesFilterQuery(m.accessFilterValueForColumn(record, col), filter) {
return false
}
}
return true
}

func (m model) matchesNamespaceFiltersExcept(entry eos.Entry, excludeColumn int) bool {
for col, filter := range m.nsFilter.filters {
if col == excludeColumn || filter == "" {
Expand Down Expand Up @@ -353,6 +392,10 @@ func (m model) groupFilterValue(g eos.GroupRecord) string {
return m.groupFilterValueForColumn(g, m.groupFilter.column)
}

func (m model) accessFilterValue(record eos.AccessRecord) string {
return m.accessFilterValueForColumn(record, m.accessFilter.column)
}

func (m model) spaceFilterValue(s eos.SpaceRecord) string {
return m.spaceFilterValueForColumn(s, m.spaceFilter.column)
}
Expand Down Expand Up @@ -403,6 +446,19 @@ func (m model) groupFilterValueForColumn(g eos.GroupRecord, column int) string {
}
}

func (m model) accessFilterValueForColumn(record eos.AccessRecord, column int) string {
switch accessFilterColumn(column) {
case accessFilterCategory:
return record.Category
case accessFilterRule:
return record.Rule
case accessFilterValue:
return record.Value
default:
return record.Category
}
}

func (m model) lessNode(a, b eos.FstRecord) bool {
var primary int
switch fstSortColumn(m.fstSort.column) {
Expand Down Expand Up @@ -902,6 +958,19 @@ func (m model) spaceFilterColumnLabel() string {
}
}

func (m model) accessFilterColumnLabel() string {
switch accessFilterColumn(m.accessFilter.column) {
case accessFilterCategory:
return "category"
case accessFilterRule:
return "rule"
case accessFilterValue:
return "value"
default:
return "category"
}
}

func (m model) spaceSortColumnLabel() string {
switch spaceSortColumn(m.spaceSort.column) {
case spaceSortName:
Expand Down Expand Up @@ -960,6 +1029,8 @@ func (m model) activeFilterColumnLabel() string {
return m.spaceFilterColumnLabel()
case viewGroups:
return m.groupFilterColumnLabel()
case viewAccess:
return m.accessFilterColumnLabel()
default:
return m.fstFilterColumnLabel()
}
Expand All @@ -984,6 +1055,9 @@ func (m *model) openFilterPopup() {
} else if m.activeView == viewGroups {
m.popup.column = m.groupsColumnSelected
m.popup.input.SetValue(m.groupFilter.filters[m.groupsColumnSelected])
} else if m.activeView == viewAccess {
m.popup.column = m.accessColumnSelected
m.popup.input.SetValue(m.accessFilter.filters[m.accessColumnSelected])
} else {
m.popup.column = m.fstColumnSelected
m.popup.input.SetValue(m.fstFilter.filters[m.fstColumnSelected])
Expand Down Expand Up @@ -1078,6 +1152,15 @@ func (m *model) applyPopupSelection() {
m.statsDetailSelected = 0
m.statsDetailOffsetX = 0
m.closeFilterPopup(fmt.Sprintf("Stats detail filters active: %d", len(m.statsFilter.filters)))
case viewAccess:
m.accessFilter.column = m.popup.column
if value == "" {
delete(m.accessFilter.filters, m.popup.column)
} else {
m.accessFilter.filters[m.popup.column] = value
}
m.accessSelected = clampIndex(0, len(m.visibleAccessRecords()))
m.closeFilterPopup(fmt.Sprintf("Access filters active: %d", len(m.accessFilter.filters)))
default:
m.fstFilter.column = m.popup.column
if value == "" {
Expand Down Expand Up @@ -1173,6 +1256,17 @@ func (m model) popupValues() []string {
values = append(values, value)
}
}
case viewAccess:
for _, record := range m.accessRecords {
if !m.matchesAccessFiltersExcept(record, m.popup.column) {
continue
}
value := m.accessFilterValueForColumn(record, m.popup.column)
if !seen[value] {
seen[value] = true
values = append(values, value)
}
}
default:
for _, node := range m.fsts {
if !m.matchesNodeFiltersExcept(node, m.popup.column) {
Expand Down
Loading