feat: Initial implementation of an attribute based authentication system
This commit is contained in:
commit
d95ef7f28f
6
attribute.go
Normal file
6
attribute.go
Normal file
@ -0,0 +1,6 @@
|
||||
package abac
|
||||
|
||||
type Attribute struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
||||
module git.canoozie.net/venturelab02/uncomplicated/abac
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
)
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
17
policy_engine.go
Normal file
17
policy_engine.go
Normal file
@ -0,0 +1,17 @@
|
||||
package abac
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Result of evaluating an ABAC policy
|
||||
type PolicyDecision bool
|
||||
|
||||
type PolicyEngine interface {
|
||||
EvaluatePolicy(ctx context.Context, userAttributes []Attribute, resourceAttributes []Attribute, action string) PolicyDecision
|
||||
}
|
||||
|
||||
const (
|
||||
Allow PolicyDecision = true
|
||||
Deny PolicyDecision = false
|
||||
)
|
32
service.go
Normal file
32
service.go
Normal file
@ -0,0 +1,32 @@
|
||||
package abac
|
||||
|
||||
import "context"
|
||||
|
||||
type Service struct {
|
||||
store Store
|
||||
policyEngine PolicyEngine
|
||||
}
|
||||
|
||||
func NewService(store Store, policyEngine PolicyEngine) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
policyEngine: policyEngine,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) HasAccess(userID, resourceID string, action string) (bool, error) {
|
||||
userAttributes, err := service.store.GetUserAttributes(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resourceAttributes, err := service.store.GetResourceAttributes(resourceID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
decision := service.policyEngine.EvaluatePolicy(ctx, userAttributes, resourceAttributes, action)
|
||||
|
||||
return bool(decision), nil
|
||||
}
|
40
simple_policy_engine.go
Normal file
40
simple_policy_engine.go
Normal file
@ -0,0 +1,40 @@
|
||||
package abac
|
||||
|
||||
type SimplePolicyEngine struct {
|
||||
rules []Rule
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Effect string `json:"effect"`
|
||||
Action string `json:"action"`
|
||||
Condition Condition `json:"condition"`
|
||||
}
|
||||
|
||||
type Condition struct {
|
||||
AttributeKey string `json:"attribute_key"`
|
||||
AttributeValue string `json:"attribute_value"`
|
||||
}
|
||||
|
||||
func (engine *SimplePolicyEngine) EvaluatePolicy(userAttributes, resourceAttributes []Attribute, action string) PolicyDecision {
|
||||
for _, rule := range engine.rules {
|
||||
if rule.Action == action {
|
||||
if rule.Condition.AttributeKey != "" && rule.Condition.AttributeValue != "" {
|
||||
attributeFound := false
|
||||
for _, attribute := range userAttributes {
|
||||
if attribute.Key == rule.Condition.AttributeKey && attribute.Value == rule.Condition.AttributeValue {
|
||||
attributeFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !attributeFound {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return PolicyDecision(rule.Effect == "Allow")
|
||||
}
|
||||
}
|
||||
|
||||
return Deny
|
||||
}
|
73
sql_policy_engine.go
Normal file
73
sql_policy_engine.go
Normal file
@ -0,0 +1,73 @@
|
||||
package abac
|
||||
|
||||
type SQLPolicyEngine struct {
|
||||
store *SQLiteStore
|
||||
}
|
||||
|
||||
func NewSQLPolicyEngine(store *SQLiteStore) *SQLPolicyEngine {
|
||||
return &SQLPolicyEngine{store: store}
|
||||
}
|
||||
|
||||
func (e *SQLPolicyEngine) EvaluatePolicy(userID, resourceID, action string) (PolicyDecision, error) {
|
||||
userAttributes, err := e.store.GetUserAttributes(userID)
|
||||
if err != nil {
|
||||
return Deny, err
|
||||
}
|
||||
|
||||
resourceAttributes, err := e.store.GetResourceAttributes(resourceID)
|
||||
if err != nil {
|
||||
return Deny, err
|
||||
}
|
||||
|
||||
rows, err := e.store.db.Query(`
|
||||
SELECT effect, condition_attribute_key, condition_attribute_value, condition_attribute_type
|
||||
FROM policies
|
||||
WHERE action = ?;
|
||||
`, action)
|
||||
if err != nil {
|
||||
return Deny, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var allowPolicyFound bool = false
|
||||
for rows.Next() {
|
||||
var effect string
|
||||
var conditionAttributeKey string
|
||||
var conditionAttributeValue string
|
||||
var conditionAttributeType string
|
||||
|
||||
err = rows.Scan(&effect, &conditionAttributeKey, &conditionAttributeValue)
|
||||
if err != nil {
|
||||
return Deny, err
|
||||
}
|
||||
|
||||
var hasAttribute bool = false
|
||||
if conditionAttributeType == "user" {
|
||||
hasAttribute = checkAttributes(userAttributes, conditionAttributeKey, conditionAttributeValue)
|
||||
} else if conditionAttributeType == "resource" {
|
||||
hasAttribute = checkAttributes(resourceAttributes, conditionAttributeKey, conditionAttributeValue)
|
||||
}
|
||||
|
||||
if effect == "Deny" && hasAttribute {
|
||||
return Deny, nil
|
||||
} else if effect == "Allow" && hasAttribute {
|
||||
allowPolicyFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if allowPolicyFound {
|
||||
return Allow, nil
|
||||
}
|
||||
|
||||
return Deny, nil
|
||||
}
|
||||
|
||||
func checkAttributes(attributes []Attribute, key, value string) bool {
|
||||
for _, attribute := range attributes {
|
||||
if attribute.Key == key && attribute.Value == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
145
sqlite_store.go
Normal file
145
sqlite_store.go
Normal file
@ -0,0 +1,145 @@
|
||||
package abac
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type SQLiteStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSQLiteStore(dbPath string) (*SQLiteStore, error) {
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store := &SQLiteStore{db: db}
|
||||
err = store.createTables(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (s *SQLiteStore) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
func (s *SQLiteStore) GetUserAttributes(userID string) ([]Attribute, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT a.key, a.value
|
||||
FROM attributes a
|
||||
JOIN user_attributes ua ON a.id = ua.attribute_id
|
||||
WHERE ua.user_id = ?;
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var attributes []Attribute
|
||||
|
||||
for rows.Next() {
|
||||
var attribute Attribute
|
||||
err := rows.Scan(&attribute.Key, &attribute.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attributes = append(attributes, attribute)
|
||||
}
|
||||
|
||||
return attributes, nil
|
||||
}
|
||||
|
||||
func (s *SQLiteStore) GetResourceAttributes(resourceID string) ([]Attribute, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT a.key, a.value
|
||||
FROM attributes a
|
||||
JOIN resource_attributes ra ON a.id = ra.attribute_id
|
||||
WHERE ra.resource_id = ?;
|
||||
`, resourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var attributes []Attribute
|
||||
|
||||
for rows.Next() {
|
||||
var attribute Attribute
|
||||
err := rows.Scan(&attribute.Key, &attribute.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attributes = append(attributes, attribute)
|
||||
}
|
||||
|
||||
return attributes, nil
|
||||
}
|
||||
|
||||
func newID() string {
|
||||
uuid, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return uuid.String()
|
||||
}
|
||||
|
||||
func (s *SQLiteStore) CreatePolicy(effect, action, conditionAttributeKey, conditionAttributeValue string) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO policies (id, effect, action, condition_attribute_key, condition_attribute_value)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
`, newID(), effect, action, conditionAttributeKey, conditionAttributeValue)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SQLiteStore) createTables(db *sql.DB) error {
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attributes (
|
||||
id TEXT PRIMARY KEY,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_attributes (
|
||||
user_id TEXT NOT NULL,
|
||||
attribute_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (attribute_id) REFERENCES attributes(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resource_attributes (
|
||||
resource_id TEXT NOT NULL,
|
||||
attribute_id TEXT NOT NULL,
|
||||
FOREIGN KEY (resource_id) REFERENCES resources(id),
|
||||
FOREIGN KEY (attribute_id) REFERENCES attributes(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policies (
|
||||
id TEXT PRIMARY KEY,
|
||||
effect TEXT NOT NULL CHECK(effect IN ('Allow', 'Deny')),
|
||||
action TEXT NOT NULL,
|
||||
condition_attribute_key TEXT,
|
||||
condition_attribute_value TEXT
|
||||
);
|
||||
`)
|
||||
return err
|
||||
}
|
154
sqlite_store_test.go
Normal file
154
sqlite_store_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
package abac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func TestNewSQLiteStore(t *testing.T) {
|
||||
dbPath := ":memory:"
|
||||
store, err := NewSQLiteStore(dbPath)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
if store.db == nil {
|
||||
t.Errorf("expected db to be not nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
dbPath := ":memory:"
|
||||
store, err := NewSQLiteStore(dbPath)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserAttributes(t *testing.T) {
|
||||
store, err := NewSQLiteStore(":memory:")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
userID := "user-id"
|
||||
attributeKey := "key"
|
||||
attributeValue := "value"
|
||||
|
||||
_, err = store.db.Exec(`
|
||||
INSERT INTO users (id, username) VALUES (?, ?);
|
||||
`, userID, "username")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
|
||||
attributeId := newID()
|
||||
_, err = store.db.Exec(`
|
||||
INSERT INTO attributes (id, key, value) VALUES (?, ?, ?);
|
||||
`, attributeId, attributeKey, attributeValue)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
|
||||
_, err = store.db.Exec(`
|
||||
INSERT INTO user_attributes (user_id, attribute_id) VALUES (?, ?);
|
||||
`, userID, attributeId)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
|
||||
attributes, err := store.GetUserAttributes(userID)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
t.Logf("attributes: %v", attributes)
|
||||
if len(attributes) == 0 {
|
||||
t.Errorf("expected at least one attribute")
|
||||
}
|
||||
if attributes[0].Key != attributeKey || attributes[0].Value != attributeValue {
|
||||
t.Errorf("expected key=%s and value=%s but got key=%s and value=%s",
|
||||
attributeKey, attributeValue, attributes[0].Key, attributes[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetResourceAttributes(t *testing.T) {
|
||||
dbPath := ":memory:"
|
||||
store, err := NewSQLiteStore(dbPath)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
resourceID := "resource-id"
|
||||
attributeKey := "key"
|
||||
attributeValue := "value"
|
||||
|
||||
_, err = store.db.Exec(`
|
||||
INSERT INTO resources (id, name) VALUES (?, ?);
|
||||
`, resourceID, "name")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
|
||||
attributeId := newID()
|
||||
_, err = store.db.Exec(`
|
||||
INSERT INTO attributes (id, key, value) VALUES (?, ?, ?);
|
||||
`, attributeId, attributeKey, attributeValue)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
|
||||
_, err = store.db.Exec(`
|
||||
INSERT INTO resource_attributes (resource_id, attribute_id) VALUES (?, ?);
|
||||
`, resourceID, attributeId)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
|
||||
attributes, err := store.GetResourceAttributes(resourceID)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
if len(attributes) == 0 {
|
||||
t.Errorf("expected at least one attribute")
|
||||
}
|
||||
if attributes[0].Key != attributeKey || attributes[0].Value != attributeValue {
|
||||
t.Errorf("expected key=%s and value=%s but got key=%s and value=%s",
|
||||
attributeKey, attributeValue, attributes[0].Key, attributes[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePolicy(t *testing.T) {
|
||||
dbPath := ":memory:"
|
||||
store, err := NewSQLiteStore(dbPath)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
effect := "Allow"
|
||||
action := "action"
|
||||
conditionAttributeKey := "key"
|
||||
conditionAttributeValue := "value"
|
||||
|
||||
err = store.CreatePolicy(effect, action, conditionAttributeKey, conditionAttributeValue)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
|
||||
var id string
|
||||
err = store.db.QueryRow(`SELECT id FROM policies;`).Scan(&id)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got %v", err)
|
||||
}
|
||||
if id == "" {
|
||||
t.Errorf("expected policy id to be not empty")
|
||||
}
|
||||
}
|
8
store.go
Normal file
8
store.go
Normal file
@ -0,0 +1,8 @@
|
||||
package abac
|
||||
|
||||
type Store interface {
|
||||
GetUserAttributes(userID string) ([]Attribute, error)
|
||||
GetRoleAttributes(roleID string) ([]Attribute, error)
|
||||
GetResourceAttributes(resourceID string) ([]Attribute, error)
|
||||
HasAccess(userID, resourceID, action string) (bool, error)
|
||||
}
|
Loading…
Reference in New Issue
Block a user