diff --git a/src/ttfrog/app.py b/src/ttfrog/app.py
index b337b88..9ee5d03 100644
--- a/src/ttfrog/app.py
+++ b/src/ttfrog/app.py
@@ -4,7 +4,7 @@ from pathlib import Path
from types import SimpleNamespace
from dotenv import dotenv_values
-from flask import Flask, session
+from flask import Flask
from flask_session import Session
from grung.db import GrungDB
from tinydb.storages import MemoryStorage
@@ -78,7 +78,7 @@ VIEW_URI=/
config=config_file,
data_root=data_root,
database=data_root / f"{self.config.NAME}.json",
- sessions=data_root / "session_cache"
+ sessions=data_root / "session_cache",
)
def initialize(self, db: GrungDB = None, force: bool = False) -> None:
@@ -86,7 +86,9 @@ VIEW_URI=/
Instantiate both the database and the flask application.
"""
if force or not self._initialized:
- if self.config.IN_MEMORY_DB:
+ if db:
+ self.db = db
+ elif self.config.IN_MEMORY_DB:
self.db = GrungDB.with_schema(schema, storage=MemoryStorage)
else:
self.db = GrungDB.with_schema(
@@ -99,7 +101,6 @@ VIEW_URI=/
self.web.config["SECRET_KEY"] = self.config.SECRET_KEY
self.web.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
self.web.config["DEBUG"] = True
-
self.web.config["SESSION_TYPE"] = "filesystem"
self.web.config["SESSION_REFRESH_EACH_REQUEST"] = True
self.web.config["SESSION_FILE_DIR"] = self.path.sessions
@@ -134,11 +135,28 @@ VIEW_URI=/
self.add_member(npcs, sabetha)
self.add_member(npcs, johns)
+ guest = self.add_member(users, schema.User(name="guest"))
+
# create the admin user and admins group
- admin = self.add_member(users, schema.User(name=self.config.ADMIN_USERNAME, email=self.config.ADMIN_EMAIL))
+ admin = self.add_member(
+ users, schema.User(name=self.config.ADMIN_USERNAME, password="fnord", email=self.config.ADMIN_EMAIL)
+ )
admins = self.db.save(schema.Group(name="administrators"))
admins = self.add_member(groups, admins)
- self.add_member(admins, admin)
+ admin = self.add_member(admins, admin)
+
+ groups.set_permissions(admins, permissions=[
+ schema.Permissions.READ,
+ schema.Permissions.WRITE,
+ schema.Permissions.DELETE
+ ], db=self.db)
+
+ users.set_permissions(admins, permissions=[
+ schema.Permissions.READ,
+ schema.Permissions.WRITE,
+ schema.Permissions.DELETE
+ ], db=self.db)
+
sys.modules[__name__] = ApplicationContext()
diff --git a/src/ttfrog/forms.py b/src/ttfrog/forms.py
index 9159c8f..1dd8a29 100644
--- a/src/ttfrog/forms.py
+++ b/src/ttfrog/forms.py
@@ -1,12 +1,11 @@
from dataclasses import dataclass, field
from functools import cached_property
+from flask import g
from grung.types import BackReference, Collection, Pointer, Record
from ttfrog import schema
-from flask import g
-
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference]
@@ -15,6 +14,7 @@ class Form:
"""
The base Form controller for the web UI.
"""
+
record: Record
data: field(default_factory=dict)
@@ -26,7 +26,6 @@ class Form:
def prepare(self):
for key, value in self.data.items():
-
# filter out fields that cannot be set by the user
if key in self.read_only:
continue
@@ -41,6 +40,7 @@ class Page(Form):
"""
A form for creating and updating Page records.
"""
+
record: schema.Page
@cached_property
@@ -53,6 +53,7 @@ class NPC(Page):
"""
A form for creating and updating Page records.
"""
+
record: schema.NPC
@@ -61,6 +62,7 @@ class User(Page):
"""
A form for creating and updating Page records.
"""
+
record: schema.NPC
@@ -69,4 +71,5 @@ class Group(Page):
"""
A form for creating and updating Page records.
"""
+
record: schema.NPC
diff --git a/src/ttfrog/schema.py b/src/ttfrog/schema.py
index 1c99a39..411ee46 100644
--- a/src/ttfrog/schema.py
+++ b/src/ttfrog/schema.py
@@ -1,5 +1,10 @@
+from __future__ import annotations
-from grung.types import BackReference, Collection, Field, Record, Pointer
+from datetime import datetime
+from typing import List
+
+from grung.types import BackReference, Collection, DateTime, Field, Password, Pointer, Record, Timestamp
+from tinydb import Query
class Page(Record):
@@ -10,15 +15,17 @@ class Page(Record):
@classmethod
def fields(cls):
return [
- *super().fields(), # Pick up the UID and whatever other non-optional fields exist
- Field("uri", unique=True), # The URI for the page, relative to the app's VIEW_URI
- Field("name"), # The portion of the URI after the last /
- Field("title"), # The page title
- Field("body"), # The main content blob of the page
- Collection("members", Page), # The pages that exist below this page's URI
+ *super().fields(), # Pick up the UID and whatever other non-optional fields exist
+ Field("uri", unique=True), # The URI for the page, relative to the app's VIEW_URI
+ Field("name"), # The portion of the URI after the last /
+ Field("title"), # The page title
+ Field("body"), # The main content blob of the page
+ Collection("members", Page), # The pages that exist below this page's URI
BackReference("parent", value_type=Page), # The page that exists above this page's URI
- Pointer("author", value_type=User), # The last user to touch the page.
- # DateTime("last_modified"), # The last time the page was modified.
+ Pointer("author", value_type=User), # The last user to touch the page.
+ DateTime("created"), # When the page was created
+ Timestamp("last_modified"), # The last time the page was modified.
+ Collection("acl", Permissions),
]
def before_insert(self, db):
@@ -29,6 +36,10 @@ class Page(Record):
"""
super().before_insert(db)
+ now = datetime.utcnow()
+ if not self.doc_id and self.created < now:
+ self.created = now
+
if not self.name and not self.title:
raise Exception("Must provide either a name or a title!")
if not self.name:
@@ -44,7 +55,7 @@ class Page(Record):
correct URI. This ensures that if a page is moved from one collection to another, the URI is updated.
"""
super().after_insert(db)
- if not hasattr(self, 'members'):
+ if not hasattr(self, "members"):
return
for child in self.members:
obj = BackReference.dereference(child, db)
@@ -57,24 +68,62 @@ class Page(Record):
return page
return None
+ def set_permissions(self, entity: Entity, permissions: List, db) -> Record:
+ perms = db.save(Permissions(entity=entity, grants="".join(permissions)))
+ self.acl = [entry for entry in self.acl if entry.entity != entity] + [perms]
+ self = db.save(self)
+ return perms
-class User(Page):
+
+class Entity(Page):
+ def has_permission(self, record: Record, requested: str, db) -> bool:
+
+ # if there's no ACL at all, the record is world-readable.
+ if not getattr(record, "acl", None):
+ return requested == Permissions.READ
+
+ # Use the grant specific to this entity, if there is one
+ for entry in record.acl:
+ if entry.entity.uid == self.uid:
+ return requested in entry.grants
+
+ for group in db.Group.search(Query()["members"].any([self.reference])):
+ if group.has_permission(record, requested, db):
+ return True
+ return False
+
+ def can_read(self, record: Record, db):
+ return self.has_permission(record, Permissions.READ, db)
+
+ def can_write(self, record: Record, db):
+ return self.has_permission(record, Permissions.WRITE, db)
+
+ def can_delete(self, record: Record, db):
+ return self.has_permission(record, Permissions.DELETE, db)
+
+
+class User(Entity):
"""
A website user, editable as a wiki page.
"""
+ def check_credentials(self, username: str, password: str) -> bool:
+ return username == self.name and self._metadata.fields["password"].compare(password, self.password)
+
@classmethod
def fields(cls):
return [
field
for field in [
*super().fields(),
- Field("email", unique=True)
- ] if field.name != "members"
+ Field("email", unique=True),
+ Password("password"),
+ ]
+ if field.name != "members"
]
-class Group(Page):
+class Group(Entity):
"""
A set of users, editable as a wiki page.
"""
@@ -84,3 +133,13 @@ class NPC(Page):
"""
An NPC, editable as a wiki page.
"""
+
+
+class Permissions(Record):
+ READ = "r"
+ WRITE = "w"
+ DELETE = "d"
+
+ @classmethod
+ def fields(cls):
+ return [*super().fields(), Pointer("entity", Entity), Field("grants")]
diff --git a/src/ttfrog/themes/default/base.html b/src/ttfrog/themes/default/base.html
index ddeafef..4a61ad1 100644
--- a/src/ttfrog/themes/default/base.html
+++ b/src/ttfrog/themes/default/base.html
@@ -16,6 +16,11 @@
/ {{ name }}
{% endfor %}
+ {% if session['user_id'] == 1 %}
+ Welcome, {{ user['name'] }}. [ LOGIN ]
+ {% else %}
+ Welcome, {{ user['name'] }}.
+ {% endif %}