Skip to content

Commit 2da1504

Browse files
committed
remove regex from authorizer api and replace magic values with constants
1 parent c8b8e1d commit 2da1504

4 files changed

Lines changed: 27 additions & 268 deletions

File tree

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ libsql = { version = "0.9.30", features = ["encryption"] }
1515
napi = { version = "2", default-features = false, features = ["napi6", "tokio_rt", "async"] }
1616
napi-derive = "2"
1717
once_cell = "1.18.0"
18-
regex = "1"
1918
serde_json = "1.0.140"
2019
tokio = { version = "1.47.1", features = [ "rt-multi-thread" ] }
2120
tracing = "0.1"

integration-tests/tests/extensions.test.js

Lines changed: 5 additions & 224 deletions
Original file line numberDiff line numberDiff line change
@@ -152,22 +152,6 @@ test.serial("Rule-based: glob pattern on table name", async (t) => {
152152
});
153153
});
154154

155-
test.serial("Rule-based: regex pattern on table name", async (t) => {
156-
const db = t.context.db;
157-
158-
db.authorizer({
159-
rules: [
160-
{ action: Action.READ, table: /^users$/, policy: Authorization.ALLOW },
161-
{ action: Action.SELECT, policy: Authorization.ALLOW },
162-
],
163-
defaultPolicy: Authorization.DENY,
164-
});
165-
166-
const stmt = db.prepare("SELECT * FROM users");
167-
const users = stmt.all();
168-
t.is(users.length, 2);
169-
});
170-
171155
test.serial("Rule-based: IGNORE returns NULL for READ columns", async (t) => {
172156
const db = t.context.db;
173157

@@ -192,7 +176,7 @@ test.serial("Rule-based: entity pattern for functions", async (t) => {
192176

193177
db.authorizer({
194178
rules: [
195-
{ action: Action.FUNCTION, entity: /^(lower|upper|length)$/, policy: Authorization.ALLOW },
179+
{ action: Action.FUNCTION, entity: "upper", policy: Authorization.ALLOW },
196180
{ action: Action.READ, policy: Authorization.ALLOW },
197181
{ action: Action.SELECT, policy: Authorization.ALLOW },
198182
],
@@ -430,193 +414,13 @@ test.serial("Glob: exact string without wildcards is exact match", async (t) =>
430414
t.is(rows.length, 2);
431415
});
432416

433-
// ---- Regex pattern tests ----
434-
435-
test.serial("Regex: case-insensitive flag", async (t) => {
436-
const db = t.context.db;
437-
438-
db.exec("CREATE TABLE IF NOT EXISTS Users_CI (id INTEGER PRIMARY KEY, val TEXT)");
439-
db.exec("INSERT INTO Users_CI (id, val) VALUES (1, 'test')");
440-
441-
db.authorizer({
442-
rules: [
443-
{ action: Action.READ, table: /^users_ci$/i, policy: Authorization.ALLOW },
444-
{ action: Action.SELECT, policy: Authorization.ALLOW },
445-
],
446-
defaultPolicy: Authorization.DENY,
447-
});
448-
449-
const rows = db.prepare("SELECT * FROM Users_CI").all();
450-
t.is(rows.length, 1);
451-
});
452-
453-
test.serial("Regex: partial match (no anchors)", async (t) => {
454-
const db = t.context.db;
455-
456-
// /user/ without anchors should match "users" (partial match)
457-
db.authorizer({
458-
rules: [
459-
{ action: Action.READ, table: /user/, policy: Authorization.ALLOW },
460-
{ action: Action.SELECT, policy: Authorization.ALLOW },
461-
],
462-
defaultPolicy: Authorization.DENY,
463-
});
464-
465-
const rows = db.prepare("SELECT * FROM users").all();
466-
t.is(rows.length, 2);
467-
});
468-
469-
test.serial("Regex: anchored pattern rejects partial matches", async (t) => {
470-
const db = t.context.db;
471-
472-
// /^user$/ should NOT match "users" (has trailing s)
473-
db.authorizer({
474-
rules: [
475-
{ action: Action.READ, table: /^user$/, policy: Authorization.ALLOW },
476-
{ action: Action.SELECT, policy: Authorization.ALLOW },
477-
],
478-
defaultPolicy: Authorization.DENY,
479-
});
480-
481-
await t.throwsAsync(async () => {
482-
return await db.prepare("SELECT * FROM users");
483-
}, { instanceOf: t.context.errorType, code: "SQLITE_AUTH" });
484-
});
485-
486-
test.serial("Regex: alternation pattern", async (t) => {
487-
const db = t.context.db;
488-
489-
db.exec("CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, pname TEXT)");
490-
db.exec("INSERT INTO products (id, pname) VALUES (1, 'Widget')");
491-
492-
db.authorizer({
493-
rules: [
494-
{ action: Action.READ, table: /^(users|products)$/, policy: Authorization.ALLOW },
495-
{ action: Action.SELECT, policy: Authorization.ALLOW },
496-
],
497-
defaultPolicy: Authorization.DENY,
498-
});
499-
500-
const u = db.prepare("SELECT * FROM users").all();
501-
t.is(u.length, 2);
502-
const p = db.prepare("SELECT * FROM products").all();
503-
t.is(p.length, 1);
504-
});
505-
506-
test.serial("Regex: character class pattern", async (t) => {
507-
const db = t.context.db;
508-
509-
db.exec("CREATE TABLE IF NOT EXISTS t1_data (id INTEGER PRIMARY KEY)");
510-
db.exec("CREATE TABLE IF NOT EXISTS t2_data (id INTEGER PRIMARY KEY)");
511-
db.exec("INSERT INTO t1_data (id) VALUES (1)");
512-
db.exec("INSERT INTO t2_data (id) VALUES (1)");
513-
514-
db.authorizer({
515-
rules: [
516-
{ action: Action.READ, table: /^t[0-9]_data$/, policy: Authorization.ALLOW },
517-
{ action: Action.SELECT, policy: Authorization.ALLOW },
518-
],
519-
defaultPolicy: Authorization.DENY,
520-
});
521-
522-
const r1 = db.prepare("SELECT * FROM t1_data").all();
523-
t.is(r1.length, 1);
524-
const r2 = db.prepare("SELECT * FROM t2_data").all();
525-
t.is(r2.length, 1);
526-
527-
// users shouldn't match
528-
await t.throwsAsync(async () => {
529-
return await db.prepare("SELECT * FROM users");
530-
}, { instanceOf: t.context.errorType, code: "SQLITE_AUTH" });
531-
});
532-
533-
test.serial("Regex: on column name with IGNORE", async (t) => {
534-
const db = t.context.db;
535-
536-
// IGNORE any column ending in "il" → email gets NULL
537-
db.authorizer({
538-
rules: [
539-
{ action: Action.READ, table: "users", column: /il$/, policy: Authorization.IGNORE },
540-
{ action: Action.READ, policy: Authorization.ALLOW },
541-
{ action: Action.SELECT, policy: Authorization.ALLOW },
542-
],
543-
defaultPolicy: Authorization.DENY,
544-
});
545-
546-
const row = db.prepare("SELECT id, name, email FROM users WHERE id = 1").get();
547-
t.is(row.id, 1);
548-
t.is(row.name, "Alice");
549-
t.is(row.email, null); // "email" ends in "il"
550-
});
551-
552-
test.serial("Regex: on entity name for allowed functions", async (t) => {
417+
test.serial("Glob: table + column combo", async (t) => {
553418
const db = t.context.db;
554419

555-
// Allow only functions starting with lowercase letters
420+
// For any table matching user*, IGNORE columns matching e*
556421
db.authorizer({
557422
rules: [
558-
{ action: Action.FUNCTION, entity: /^[a-z]/, policy: Authorization.ALLOW },
559-
{ action: Action.READ, policy: Authorization.ALLOW },
560-
{ action: Action.SELECT, policy: Authorization.ALLOW },
561-
],
562-
defaultPolicy: Authorization.DENY,
563-
});
564-
565-
const row = db.prepare("SELECT length(name) as len FROM users WHERE id = 1").get();
566-
t.is(row.len, 5); // "Alice" = 5 chars
567-
});
568-
569-
test.serial("Regex: non-matching regex denies correctly", async (t) => {
570-
const db = t.context.db;
571-
572-
// Only allow tables starting with "archive_"
573-
db.authorizer({
574-
rules: [
575-
{ action: Action.READ, table: /^archive_/, policy: Authorization.ALLOW },
576-
{ action: Action.SELECT, policy: Authorization.ALLOW },
577-
],
578-
defaultPolicy: Authorization.DENY,
579-
});
580-
581-
// users doesn't start with archive_
582-
await t.throwsAsync(async () => {
583-
return await db.prepare("SELECT * FROM users");
584-
}, { instanceOf: t.context.errorType, code: "SQLITE_AUTH" });
585-
});
586-
587-
test.serial("Regex: complex pattern with quantifiers", async (t) => {
588-
const db = t.context.db;
589-
590-
db.exec("CREATE TABLE IF NOT EXISTS logs_2024_01 (id INTEGER PRIMARY KEY, msg TEXT)");
591-
db.exec("INSERT INTO logs_2024_01 (id, msg) VALUES (1, 'jan')");
592-
593-
// Match logs_YYYY_MM pattern
594-
db.authorizer({
595-
rules: [
596-
{ action: Action.READ, table: /^logs_\d{4}_\d{2}$/, policy: Authorization.ALLOW },
597-
{ action: Action.SELECT, policy: Authorization.ALLOW },
598-
],
599-
defaultPolicy: Authorization.DENY,
600-
});
601-
602-
const rows = db.prepare("SELECT * FROM logs_2024_01").all();
603-
t.is(rows.length, 1);
604-
605-
// users doesn't match the date pattern
606-
await t.throwsAsync(async () => {
607-
return await db.prepare("SELECT * FROM users");
608-
}, { instanceOf: t.context.errorType, code: "SQLITE_AUTH" });
609-
});
610-
611-
// ---- Combined glob/regex with multiple fields ----
612-
613-
test.serial("Glob table + regex column combo", async (t) => {
614-
const db = t.context.db;
615-
616-
// For any table matching user*, IGNORE columns matching a secret-ish pattern
617-
db.authorizer({
618-
rules: [
619-
{ action: Action.READ, table: "user*", column: /^(email|password|ssn)$/, policy: Authorization.IGNORE },
423+
{ action: Action.READ, table: "user*", column: "e*", policy: Authorization.IGNORE },
620424
{ action: Action.READ, policy: Authorization.ALLOW },
621425
{ action: Action.SELECT, policy: Authorization.ALLOW },
622426
],
@@ -626,26 +430,7 @@ test.serial("Glob table + regex column combo", async (t) => {
626430
const row = db.prepare("SELECT id, name, email FROM users WHERE id = 2").get();
627431
t.is(row.id, 2);
628432
t.is(row.name, "Bob");
629-
t.is(row.email, null); // email matched the regex, users matched user*
630-
});
631-
632-
test.serial("Regex table + glob column combo", async (t) => {
633-
const db = t.context.db;
634-
635-
// For tables matching /^users$/, IGNORE columns matching e*
636-
db.authorizer({
637-
rules: [
638-
{ action: Action.READ, table: /^users$/, column: "e*", policy: Authorization.IGNORE },
639-
{ action: Action.READ, policy: Authorization.ALLOW },
640-
{ action: Action.SELECT, policy: Authorization.ALLOW },
641-
],
642-
defaultPolicy: Authorization.DENY,
643-
});
644-
645-
const row = db.prepare("SELECT id, name, email FROM users WHERE id = 1").get();
646-
t.is(row.id, 1);
647-
t.is(row.name, "Alice");
648-
t.is(row.email, null);
433+
t.is(row.email, null); // email matches e*, users matches user*
649434
});
650435

651436
test.serial("Glob: wildcard-only pattern * matches everything", async (t) => {
@@ -700,11 +485,7 @@ test.beforeEach(async (t) => {
700485
DROP TABLE IF EXISTS audit_users;
701486
DROP TABLE IF EXISTS app_prod_logs;
702487
DROP TABLE IF EXISTS x_data_y;
703-
DROP TABLE IF EXISTS Users_CI;
704488
DROP TABLE IF EXISTS products;
705-
DROP TABLE IF EXISTS t1_data;
706-
DROP TABLE IF EXISTS t2_data;
707-
DROP TABLE IF EXISTS logs_2024_01;
708489
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)
709490
`);
710491
db.exec(

src/auth.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,13 @@ pub enum PatternMatcher {
2020
Exact(String),
2121
/// Glob pattern (supports `*` and `?` wildcards).
2222
Glob(String),
23-
/// Compiled regular expression.
24-
Regex(regex::Regex),
2523
}
2624

2725
impl PatternMatcher {
2826
pub fn matches(&self, value: &str) -> bool {
2927
match self {
3028
PatternMatcher::Exact(s) => s == value,
3129
PatternMatcher::Glob(pattern) => glob_match::glob_match(pattern, value),
32-
PatternMatcher::Regex(re) => re.is_match(value),
3330
}
3431
}
3532
}
@@ -404,7 +401,25 @@ impl AuthorizerBuilder {
404401

405402
// Table-bearing action codes (actions where the old authorizer checked tables)
406403
let table_actions: Vec<i32> = vec![
407-
1, 2, 3, 4, 5, 7, 9, 10, 11, 12, 13, 14, 16, 18, 20, 23, 26, 29, 30,
404+
SQLITE_CREATE_INDEX,
405+
SQLITE_CREATE_TABLE,
406+
SQLITE_CREATE_TEMP_INDEX,
407+
SQLITE_CREATE_TEMP_TABLE,
408+
SQLITE_CREATE_TEMP_TRIGGER,
409+
SQLITE_CREATE_TRIGGER,
410+
SQLITE_DELETE,
411+
SQLITE_DROP_INDEX,
412+
SQLITE_DROP_TABLE,
413+
SQLITE_DROP_TEMP_INDEX,
414+
SQLITE_DROP_TEMP_TABLE,
415+
SQLITE_DROP_TEMP_TRIGGER,
416+
SQLITE_DROP_TRIGGER,
417+
SQLITE_INSERT,
418+
SQLITE_READ,
419+
SQLITE_UPDATE,
420+
SQLITE_ALTER_TABLE,
421+
SQLITE_CREATE_VTABLE,
422+
SQLITE_DROP_VTABLE,
408423
];
409424

410425
// Deny rules first
@@ -431,7 +446,7 @@ impl AuthorizerBuilder {
431446

432447
// Legacy behavior: always allow SELECT (no table context)
433448
rules.push(AuthRule {
434-
actions: vec![21], // SELECT
449+
actions: vec![SQLITE_SELECT],
435450
table: None,
436451
column: None,
437452
entity: None,

src/lib.rs

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -727,56 +727,20 @@ fn parse_single_rule(rule_obj: &napi::JsObject) -> Result<crate::auth::AuthRule>
727727
})
728728
}
729729

730-
/// Parse a pattern value: string (exact or glob) or RegExp.
730+
/// Parse a pattern value: plain string or glob (auto-detected by `*` or `?`).
731731
fn parse_pattern(val: JsUnknown, field_name: &str) -> Result<crate::auth::PatternMatcher> {
732732
match val.get_type()? {
733733
ValueType::String => {
734734
let s: napi::JsString = val.coerce_to_string()?;
735735
let owned = s.into_utf8()?.into_owned()?;
736-
// Auto-detect glob: if the string contains * or ?, treat as glob
737736
if owned.contains('*') || owned.contains('?') {
738737
Ok(crate::auth::PatternMatcher::Glob(owned))
739738
} else {
740739
Ok(crate::auth::PatternMatcher::Exact(owned))
741740
}
742741
}
743-
ValueType::Object => {
744-
// Check if it's a RegExp by checking for .source property
745-
let obj: napi::JsObject = val.coerce_to_object()?;
746-
if obj.has_named_property("source")? {
747-
let source_js: napi::JsString = obj.get_named_property("source")?;
748-
let source = source_js.into_utf8()?.into_owned()?;
749-
750-
// Check for flags (we support 'i' for case-insensitive)
751-
let flags_str = if obj.has_named_property("flags")? {
752-
let flags_js: napi::JsString = obj.get_named_property("flags")?;
753-
flags_js.into_utf8()?.into_owned()?
754-
} else {
755-
String::new()
756-
};
757-
758-
let pattern = if flags_str.contains('i') {
759-
format!("(?i){}", source)
760-
} else {
761-
source
762-
};
763-
764-
let re = regex::Regex::new(&pattern).map_err(|e| {
765-
napi::Error::from_reason(format!(
766-
"Invalid regex pattern for {}: {}",
767-
field_name, e
768-
))
769-
})?;
770-
Ok(crate::auth::PatternMatcher::Regex(re))
771-
} else {
772-
Err(napi::Error::from_reason(format!(
773-
"{} must be a string or RegExp",
774-
field_name
775-
)))
776-
}
777-
}
778742
_ => Err(napi::Error::from_reason(format!(
779-
"{} must be a string or RegExp",
743+
"{} must be a string",
780744
field_name
781745
))),
782746
}

0 commit comments

Comments
 (0)