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