add per user group management

Drop the secret from groups, instead set per user access rights to
read or write group information.
This commit is contained in:
ducklet 2021-08-03 17:05:25 +02:00
parent 14f2395fa6
commit e2a3f0b6fa
3 changed files with 98 additions and 22 deletions

View file

@ -6,6 +6,7 @@ from typing import (
Annotated, Annotated,
Any, Any,
ClassVar, ClassVar,
Literal,
Optional, Optional,
Type, Type,
TypeVar, TypeVar,
@ -283,6 +284,11 @@ class User:
imdb_id: str = None imdb_id: str = None
name: str = None # canonical user name name: str = None # canonical user name
secret: str = None secret: str = None
groups: list[dict[str, str]] = field(default_factory=list)
def has_access(self, group_id: Union[ULID, str], access: Literal["r", "w"] = "r"):
group_id = group_id if isinstance(group_id, str) else str(group_id)
return any(g["id"] == group_id and access in g["access"] for g in self.groups)
@dataclass @dataclass
@ -291,5 +297,4 @@ class Group:
id: ULID = field(default_factory=ULID) id: ULID = field(default_factory=ULID)
name: str = None name: str = None
secret: str = None
users: list[dict[str, str]] = field(default_factory=list) users: list[dict[str, str]] = field(default_factory=list)

View file

@ -0,0 +1,45 @@
-- add group admins
--- remove secrets from groups
CREATE TABLE _migrate_groups (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
users TEXT NOT NULL -- JSON array
);;
INSERT INTO _migrate_groups
SELECT
id,
name,
users
FROM groups
WHERE true;;
DROP TABLE groups;;
ALTER TABLE _migrate_groups
RENAME TO groups;;
--- add group access to users
CREATE TABLE _migrate_users (
id TEXT PRIMARY KEY NOT NULL,
imdb_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
secret TEXT NOT NULL,
groups TEXT NOT NULL -- JSON array
);;
INSERT INTO _migrate_users
SELECT
id,
imdb_id,
name,
secret,
'[]' AS groups
FROM users
WHERE true;;
DROP TABLE users;;
ALTER TABLE _migrate_users
RENAME TO users;;

View file

@ -37,8 +37,8 @@ log = logging.getLogger(__name__)
class AuthedUser(BaseUser): class AuthedUser(BaseUser):
def __init__(self, username: str, secret: str): def __init__(self, user_id: str, secret: str):
self.username = username self.user_id = user_id
self.secret = secret self.secret = secret
@ -154,10 +154,23 @@ async def json_from_body(request, keys: list[str] = None):
raise HTTPException(422, f"Missing data for key: {err.args[0]}") raise HTTPException(422, f"Missing data for key: {err.args[0]}")
def auth(request, secret: str = None): def is_admin(request):
is_admin = "admin" in request.auth.scopes return "admin" in request.auth.scopes
is_owner = secret and phc_compare(secret=request.user.secret, phc_string=secret)
return is_admin, bool(is_owner)
async def auth_user(request) -> Optional[User]:
if not isinstance(request.user, AuthedUser):
return
user = await db.get(User, id=request.user.user_id)
if not user:
return
is_authed = phc_compare(secret=request.user.secret, phc_string=user.secret)
if not is_authed:
return
return user
_routes = [] _routes = []
@ -376,21 +389,32 @@ async def modify_user(request):
user_id = as_ulid(request.path_params["user_id"]) user_id = as_ulid(request.path_params["user_id"])
if is_admin(request):
user = await db.get(User, id=str(user_id)) user = await db.get(User, id=str(user_id))
else:
user = await auth_user(request)
if not user: if not user:
return not_found() return not_found()
is_admin, is_owner = auth(request, user.secret) is_allowed = user.id == user_id
if not (is_admin or is_owner): if not is_allowed:
return forbidden() return forbidden()
data = await json_from_body(request) data = await json_from_body(request)
if is_admin and "name" in data: if "name" in data:
if not is_admin(request):
return forbidden("Changing user name is not allowed.")
# XXX restrict name # XXX restrict name
user.name = data["name"] user.name = data["name"]
if is_admin and "imdb_id" in data: if "imdb_id" in data:
if not is_admin(request):
return forbidden("Changing IMDb ID is not allowed.")
# XXX check if imdb_id is well-formed # XXX check if imdb_id is well-formed
user.imdb_id = data["imdb_id"] user.imdb_id = data["imdb_id"]
@ -447,29 +471,31 @@ async def add_group(request):
# XXX restrict name # XXX restrict name
secret = secrets.token_bytes() group = Group(name=name)
group = Group(name=name, secret=phc_scrypt(secret))
await db.add(group) await db.add(group)
return JSONResponse( return JSONResponse(asplain(group))
{
"secret": b64encode(secret),
"group": asplain(group),
}
)
@route("/groups/{group_id}/users", methods=["POST"]) @route("/groups/{group_id}/users", methods=["POST"])
@requires(["authenticated"]) @requires(["authenticated"])
async def add_user_to_group(request): async def add_user_to_group(request):
group_id = as_ulid(request.path_params["group_id"]) group_id = as_ulid(request.path_params["group_id"])
group = await db.get(Group, id=str(group_id)) group = await db.get(Group, id=str(group_id))
if not group: if not group:
return not_found() return not_found()
is_allowed = any(auth(request, group.secret)) is_allowed = is_admin(request)
if not is_allowed:
user = await auth_user(request)
if not user:
return not_found("User not found.")
is_allowed = user.has_access(group_id, "w")
if not is_allowed: if not is_allowed:
return forbidden() return forbidden()