Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
larsw committed Jan 14, 2024
0 parents commit 2754e43
Show file tree
Hide file tree
Showing 10 changed files with 551 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/
25 changes: 25 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright (c) 2018 Lars Wilhelmsen <[email protected]>

Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test:
go test -v
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Accumulo Access Expressions for Go

## Introduction

This package provides a simple way to parse and evaluate Accumulo access expressions in Go, based on the [AccessExpression specification](https://github.com/apache/accumulo-access/blob/main/SPECIFICATION.md).

## Usage

```go
package main

import (
"fmt"
accumulo "github.com/larsw/accumulo-access-go/pkg"
)

func main() {
res, err := accumulo.CheckAuthorization("A & B & (C | D)", "A,B,C")
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
// Print the result
fmt.Printf("%v\n", res)
}
```

* Lars Wilhelmsen (https://github.com/larsw/)

## License

Licensed under the MIT License [LICENSE_MIT](LICENSE).

3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/larsw/accumulo-access-go

go 1.18
50 changes: 50 additions & 0 deletions pkg/check_authorization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Package pkg Copyright 2024 Lars Wilhelmsen <[email protected]>. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in the LICENSE file.
package pkg

import "strings"

// CheckAuthorization checks if the given authorizations are allowed to perform the given expression.
// Arguments:
//
// expression: The expression to check.
// authorizations: A comma-separated list of authorizations.
//
// Returns:
//
// True if the authorizations are allowed to perform the expression, false otherwise.
func CheckAuthorization(expression string, authorizations string) (bool, error) {
parser := NewParser(newLexer(expression))
ast, err := parser.Parse()
if err != nil {
return false, err
}
authorizationMap := make(map[string]bool)
for _, authorization := range strings.Split(authorizations, ",") {
authorizationMap[authorization] = true
}
return ast.Evaluate(authorizationMap), nil
}

// PrepareAuthorizationCheck returns a function that can be used to check if the given authorizations are allowed to perform the given expression.
// Arguments:
//
// authorizations: A comma-separated list of authorizations.
//
// Returns:
//
// A function that can be used to check if the given authorizations are allowed to perform the given expression.
func PrepareAuthorizationCheck(authorizations string) func(string) (bool, error) {
authorizationMap := make(map[string]bool)
for _, authorization := range strings.Split(authorizations, ",") {
authorizationMap[authorization] = true
}
return func(expression string) (bool, error) {
parser := NewParser(newLexer(expression))
ast, err := parser.Parse()
if err != nil {
return false, err
}
return ast.Evaluate(authorizationMap), nil
}
}
47 changes: 47 additions & 0 deletions pkg/check_authorization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package pkg

import (
"fmt"
"testing"
)

type testCase struct {
expression string
authorizations string
expected bool
}

func TestCheckAuthorization(t *testing.T) {
testCases := []testCase{
{"label1", "label1", true},
{"label1|label2", "label1", true},
{"label1&label2", "label1", false},
{"label1&label2", "label1,label2", true},
{"label1&(label2 | label3)", "label1", false},
{"label1&(label2 | label3)", "label1,label3", true},
{"label1&(label2 | label3)", "label1,label2", true},
{"(label2 | label3)", "label1", false},
{"(label2 | label3)", "label2", true},
{"(label2 & label3)", "label2", false},
{"((label2 | label3))", "label2", true},
{"((label2 & label3))", "label2", false},
{"(((((label2 & label3)))))", "label2", false},
{"(a & b) & (c & d)", "a,b,c,d", true},
{"(a & b) & (c & d)", "a,b,c", false},
{"(a & b) | (c & d)", "a,b,d", true},
{"(a | b) & (c | d)", "a,d", true},
{"\"a b c\"", "\"a b c\"", true},
}

for _, tc := range testCases {
t.Run(fmt.Sprintf("\"%v\" + \"%v\" -> %v", tc.expression, tc.authorizations, tc.expected), func(t *testing.T) {
result, err := CheckAuthorization(tc.expression, tc.authorizations)
if err != nil {
t.Fatal(err)
}
if result != tc.expected {
t.Fatalf("expected %v for %s with %s", tc.expected, tc.expression, tc.authorizations)
}
})
}
}
160 changes: 160 additions & 0 deletions pkg/lexer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Package pkg Copyright 2024 Lars Wilhelmsen <[email protected]>. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in the LICENSE file.
package pkg

import (
"fmt"
"unicode"
)

// Token represents the different types of tokens.
type Token int

// Define token constants.
const (
AccessToken Token = iota
OpenParen
CloseParen
And
Or
None
Error
)

//func (t Token) String() string {
// switch t {
// case AccessToken:
// return "AccessToken"
// case OpenParen:
// return "("
// case CloseParen:
// return ")"
// case And:
// return "&"
// case Or:
// return "|"
// case None:
// return "None"
// default:
// return "Unknown"
// }
//}

// Lexer represents a lexer for tokenizing strings.
type Lexer struct {
input string
pos int
readPos int
ch byte
}

// newLexer creates a new Lexer instance.
func newLexer(input string) *Lexer {
l := &Lexer{input: input}
l.readChar()
return l
}

// LexerError represents an error that occurred during lexing.
type LexerError struct {
Char byte
Position int
}

func (e LexerError) Error() string {
return fmt.Sprintf("unexpected character '%c' at position %d", e.Char, e.Position)
}

func (l *Lexer) readChar() {
if l.readPos >= len(l.input) {
l.ch = 0
} else {
l.ch = l.input[l.readPos]
}
l.pos = l.readPos
l.readPos++
}

func (l *Lexer) peekChar() byte {
if l.readPos >= len(l.input) {
return 0
}
return l.input[l.readPos]
}

func (l *Lexer) nextToken() (Token, string, error) {
l.skipWhitespace()

switch l.ch {
case '(':
l.readChar()
return OpenParen, "", nil
case ')':
l.readChar()
return CloseParen, "", nil
case '&':
l.readChar()
return And, "", nil
case '|':
l.readChar()
return Or, "", nil
case 0:
return None, "", nil
case '"':
strLiteral := l.readString()
return AccessToken, strLiteral, nil
default:
if isLegalTokenLetter(l.ch) {
val := l.readIdentifier()
return AccessToken, val, nil
} else {
return Error, "", LexerError{Char: l.ch, Position: l.pos}
}
}
}

func (l *Lexer) readIdentifier() string {
startPos := l.pos
for isLegalTokenLetter(l.ch) {
l.readChar()
}
return l.input[startPos:l.pos]
}

func (l *Lexer) readString() string {
startPos := l.pos + 1 // Skip initial double quote
for {
l.readChar()
if l.ch == '"' || l.ch == 0 {
break // End of string or end of input
}

// Handle escape sequences
if l.ch == '\\' {
l.readChar()
if l.ch != '"' && l.ch != '\\' {
// Handle invalid escape sequence
return l.input[startPos : l.pos-1] // Return string up to invalid escape
}
}
}
str := l.input[startPos:l.pos]
l.readChar() // Skip closing double quote
return str
}

func (l *Lexer) skipWhitespace() {
for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
l.readChar()
}
}

func isLegalTokenLetter(ch byte) bool {

return unicode.IsLetter(rune(ch)) ||
unicode.IsDigit(rune(ch)) ||
ch == '_' ||
ch == '-' ||
ch == '.' ||
ch == ':'
}
48 changes: 48 additions & 0 deletions pkg/lexer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package pkg

import "testing"

func TestLexer(t *testing.T) {
lexer := newLexer("(a & b) | c")
token, _, _ := lexer.nextToken()
if token != OpenParen {
t.Fatal("expected OpenParen")
}
token, val, _ := lexer.nextToken()
if token != AccessToken {
t.Fatal("expected AccessToken")
}
if val != "a" {
t.Fatal("expected a")
}
token, _, _ = lexer.nextToken()
if token != And {
t.Fatal("expected And")
}
token, val, _ = lexer.nextToken()
if token != AccessToken {
t.Fatal("expected AccessToken")
}
if val != "b" {
t.Fatal("expected b")
}
token, _, _ = lexer.nextToken()
if token != CloseParen {
t.Fatal("expected CloseParen")
}
token, _, _ = lexer.nextToken()
if token != Or {
t.Fatal("expected Or")
}
token, val, _ = lexer.nextToken()
if token != AccessToken {
t.Fatal("expected AccessToken")
}
if val != "c" {
t.Fatal("expected c")
}
token, _, _ = lexer.nextToken()
if token != None {
t.Fatal("expected end of input")
}
}
Loading

0 comments on commit 2754e43

Please sign in to comment.