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 %}