From 44156b4bc85953c3fd3f05c21a40770db7f6f0d1 Mon Sep 17 00:00:00 2001 From: Sekou Date: Wed, 16 Sep 2020 22:32:32 -0400 Subject: [PATCH] WIP --- .../app/auth/{{cookiecutter.app}}/config.json | 11 - .../services/auth/__init__.py | 3 - .../services/auth/security/acls.py | 39 ---- .../default/{{cookiecutter.app}}/config.json | 11 - cookiecutter/auth/cookiecutter.json | 6 + .../{{cookiecutter.auth}}}/__init__.py | 0 .../auth/{{cookiecutter.auth}}/acls.py | 17 ++ .../{{cookiecutter.auth}}}/endpoints.py | 0 .../{{cookiecutter.auth}}}/model.py | 8 +- .../{{cookiecutter.auth}}}/roles.py | 0 .../auth/{{cookiecutter.auth}}/utils.py | 0 poetry.lock | 21 +- pyproject.toml | 1 + .../compass/compass/services/auth/__init__.py | 3 - .../compass/compass/services/auth/acls.py | 24 +-- .../compass/compass/services/auth/model.py | 8 +- .../{member/acls.py => auth/utils.py} | 0 snippets/compass/config.json | 27 +-- snippets/compass/poetry.lock | 202 ++++++++++++++++++ snippets/compass/pyproject.toml | 1 + snippets/netizen/config.json | 72 +++++++ snippets/netizen/netizen/cli.py | 17 ++ snippets/netizen/netizen/services/__init__.py | 0 .../netizen/netizen/services/auth/__init__.py | 0 .../netizen/netizen/services/auth/acls.py | 17 ++ .../netizen/services/auth/endpoints.py | 134 ++++++++++++ .../netizen/netizen/services/auth/model.py | 64 ++++++ .../netizen/netizen/services/auth/roles.py | 0 .../netizen/netizen/services/auth/utils.py | 0 .../netizen/services/items/__init__.py | 0 .../netizen/netizen/services/items/acls.py | 0 .../services/items}/endpoints/__init__.py | 0 .../services/items}/endpoints/utils.py | 2 +- .../netizen/services/items}/model.py | 4 +- .../netizen/services/members/__init__.py | 0 .../netizen/netizen/services/members/acls.py | 0 .../services/members/endpoints/__init__.py | 1 + .../services/members/endpoints/utils.py | 14 ++ .../netizen/netizen/services/members/model.py | 6 + snippets/netizen/pyproject.toml | 19 ++ sowba/cli/__init__.py | 15 +- sowba/cli/auth.py | 172 +++++++++++++++ sowba/cli/utils.py | 6 + sowba/core/path.py | 4 + sowba/security/acl.py | 1 + sowba/security/roles.py | 24 ++- sowba/settings/model.py | 27 +-- 47 files changed, 832 insertions(+), 149 deletions(-) delete mode 100644 cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/__init__.py delete mode 100644 cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security/acls.py create mode 100644 cookiecutter/auth/cookiecutter.json rename cookiecutter/{app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security => auth/{{cookiecutter.auth}}}/__init__.py (100%) create mode 100644 cookiecutter/auth/{{cookiecutter.auth}}/acls.py rename cookiecutter/{app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth => auth/{{cookiecutter.auth}}}/endpoints.py (100%) rename cookiecutter/{app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth => auth/{{cookiecutter.auth}}}/model.py (85%) rename cookiecutter/{app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security => auth/{{cookiecutter.auth}}}/roles.py (100%) rename snippets/compass/compass/services/member/__init__.py => cookiecutter/auth/{{cookiecutter.auth}}/utils.py (100%) rename snippets/compass/compass/services/{member/acls.py => auth/utils.py} (100%) create mode 100644 snippets/compass/poetry.lock create mode 100644 snippets/netizen/config.json create mode 100644 snippets/netizen/netizen/cli.py create mode 100644 snippets/netizen/netizen/services/__init__.py create mode 100644 snippets/netizen/netizen/services/auth/__init__.py create mode 100644 snippets/netizen/netizen/services/auth/acls.py create mode 100644 snippets/netizen/netizen/services/auth/endpoints.py create mode 100644 snippets/netizen/netizen/services/auth/model.py create mode 100644 snippets/netizen/netizen/services/auth/roles.py create mode 100644 snippets/netizen/netizen/services/auth/utils.py create mode 100644 snippets/netizen/netizen/services/items/__init__.py create mode 100644 snippets/netizen/netizen/services/items/acls.py rename snippets/{compass/compass/services/member => netizen/netizen/services/items}/endpoints/__init__.py (100%) rename snippets/{compass/compass/services/member => netizen/netizen/services/items}/endpoints/utils.py (83%) rename snippets/{compass/compass/services/member => netizen/netizen/services/items}/model.py (51%) create mode 100644 snippets/netizen/netizen/services/members/__init__.py create mode 100644 snippets/netizen/netizen/services/members/acls.py create mode 100644 snippets/netizen/netizen/services/members/endpoints/__init__.py create mode 100644 snippets/netizen/netizen/services/members/endpoints/utils.py create mode 100644 snippets/netizen/netizen/services/members/model.py create mode 100644 snippets/netizen/pyproject.toml create mode 100644 sowba/cli/auth.py diff --git a/cookiecutter/app/auth/{{cookiecutter.app}}/config.json b/cookiecutter/app/auth/{{cookiecutter.app}}/config.json index 0aa1a7c..0fd924f 100644 --- a/cookiecutter/app/auth/{{cookiecutter.app}}/config.json +++ b/cookiecutter/app/auth/{{cookiecutter.app}}/config.json @@ -30,17 +30,6 @@ } ] }, - "auth": { - "admin": { - "username": "admin", - "email": "admin@sowba.com" - }, - "security": { - "algorithm": "HS256", - "access_token_expire_minutes": 30, - "auth_path": "/token" - } - }, "asgi": { "host": "0.0.0.0", "port": 8000, diff --git a/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/__init__.py b/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/__init__.py deleted file mode 100644 index 9182009..0000000 --- a/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from sowba.core.utils import get_service_router - -router = get_service_router("users") diff --git a/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security/acls.py b/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security/acls.py deleted file mode 100644 index 883a775..0000000 --- a/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security/acls.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import List, Callable -import dataclasses as dcls -from collections import namedtuple -from fastapi_permissions import Allow, Authenticated -from pydantic import BaseModel - - -class Acl(namedtuple("Acl", ("access", "principal", "permission"))): - __slots__ = () - - -class CreateItemAcl: - __acl__ = [ - Acl(Allow, "role:user", "create"), - Acl(Allow, "role:admin", "create"), - ] - - -@dcls.dataclass -class AclPolicy: - get: Callable = None - create: CreateItemAcl = None - - -class BaseResourceAcl(BaseModel): - def __acl__(self): - assert self.owner is not None, "Owner must be define." - return [ - Acl(Allow, "role:user", "create"), - Acl(Allow, "role:admin", "create"), - Acl(Allow, Authenticated, "view"), - Acl(Allow, "role:admin", "update"), - Acl(Allow, f"user:{self.owner}", "update"), - Acl(Allow, "role:admin", "delete"), - Acl(Allow, f"user:{self.owner}", "delete"), - ] - - -default_acl_policy = AclPolicy(create=CreateItemAcl) diff --git a/cookiecutter/app/default/{{cookiecutter.app}}/config.json b/cookiecutter/app/default/{{cookiecutter.app}}/config.json index 0aa1a7c..0fd924f 100644 --- a/cookiecutter/app/default/{{cookiecutter.app}}/config.json +++ b/cookiecutter/app/default/{{cookiecutter.app}}/config.json @@ -30,17 +30,6 @@ } ] }, - "auth": { - "admin": { - "username": "admin", - "email": "admin@sowba.com" - }, - "security": { - "algorithm": "HS256", - "access_token_expire_minutes": 30, - "auth_path": "/token" - } - }, "asgi": { "host": "0.0.0.0", "port": 8000, diff --git a/cookiecutter/auth/cookiecutter.json b/cookiecutter/auth/cookiecutter.json new file mode 100644 index 0000000..184fd8d --- /dev/null +++ b/cookiecutter/auth/cookiecutter.json @@ -0,0 +1,6 @@ +{ + "auth": "{{cookiecutter.auth}}", + "storage": "{{cookiecutter.storage}}", + "algorithm": "{{cookiecutter.algorithm}}", + "access_token_expire_minutes": "{{cookiecutter.access_token_expire_minutes}}" +} \ No newline at end of file diff --git a/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security/__init__.py b/cookiecutter/auth/{{cookiecutter.auth}}/__init__.py similarity index 100% rename from cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security/__init__.py rename to cookiecutter/auth/{{cookiecutter.auth}}/__init__.py diff --git a/cookiecutter/auth/{{cookiecutter.auth}}/acls.py b/cookiecutter/auth/{{cookiecutter.auth}}/acls.py new file mode 100644 index 0000000..28b98ca --- /dev/null +++ b/cookiecutter/auth/{{cookiecutter.auth}}/acls.py @@ -0,0 +1,17 @@ +from sowba.security.acl import Acl + + +class CreateItemAcl: + __acl__ = [ + Acl(Allow, "role:user", "create"), + Acl(Allow, "role:admin", "create"), + ] + + +@dcls.dataclass +class AclPolicy: + get: Callable = None + create: CreateItemAcl = None + + +default_acl_policy = AclPolicy(create=CreateItemAcl) diff --git a/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/endpoints.py b/cookiecutter/auth/{{cookiecutter.auth}}/endpoints.py similarity index 100% rename from cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/endpoints.py rename to cookiecutter/auth/{{cookiecutter.auth}}/endpoints.py diff --git a/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/model.py b/cookiecutter/auth/{{cookiecutter.auth}}/model.py similarity index 85% rename from cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/model.py rename to cookiecutter/auth/{{cookiecutter.auth}}/model.py index cada339..fa33ff6 100644 --- a/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/model.py +++ b/cookiecutter/auth/{{cookiecutter.auth}}/model.py @@ -1,13 +1,13 @@ from typing import List from datetime import datetime from contextvars import ContextVar -from sowba.storage.utils import init_db, get_db from pydantic import BaseModel, Field, SecretStr, EmailStr from fastapi_permissions import Allow +from sowba.security import roles class UserPrincipals(BaseModel): - principals: List[str] = ["role:user"] + principals: List[str] = [roles.user] class Updateprincipals(BaseModel): @@ -56,9 +56,9 @@ class TokenData(BaseModel): class UserListAcl: - __acl__ = [(Allow, "role:admin", "view")] + __acl__ = [(Allow, roles.admin, "view")] class UserprincipalsAcl: - __acl__ = [(Allow, "role:admin", "update")] + __acl__ = [(Allow, roles.admin, "update")] diff --git a/cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security/roles.py b/cookiecutter/auth/{{cookiecutter.auth}}/roles.py similarity index 100% rename from cookiecutter/app/auth/{{cookiecutter.app}}/{{cookiecutter.app}}/services/auth/security/roles.py rename to cookiecutter/auth/{{cookiecutter.auth}}/roles.py diff --git a/snippets/compass/compass/services/member/__init__.py b/cookiecutter/auth/{{cookiecutter.auth}}/utils.py similarity index 100% rename from snippets/compass/compass/services/member/__init__.py rename to cookiecutter/auth/{{cookiecutter.auth}}/utils.py diff --git a/poetry.lock b/poetry.lock index f24c18c..57ce485 100644 --- a/poetry.lock +++ b/poetry.lock @@ -273,6 +273,21 @@ dev = ["python-jose (>=3.1.0,<4.0.0)", "passlib (>=1.7.2,<2.0.0)", "autoflake (> doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.5.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.2.0)", "typer (>=0.3.0,<0.4.0)", "typer-cli (>=0.0.9,<0.0.10)", "pyyaml (>=5.3.1,<6.0.0)"] test = ["pytest (5.4.3)", "pytest-cov (2.10.0)", "pytest-asyncio (>=0.14.0,<0.15.0)", "mypy (0.782)", "flake8 (>=3.8.3,<4.0.0)", "black (19.10b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.15.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "databases (>=0.3.2,<0.4.0)", "orjson (>=3.2.1,<4.0.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "aiofiles (>=0.5.0,<0.6.0)", "flask (>=1.1.2,<2.0.0)"] +[[package]] +category = "main" +description = "Row Level Permissions for FastAPI" +name = "fastapi-permissions" +optional = false +python-versions = ">=3.6" +version = "0.2.6" + +[package.dependencies] +fastapi = ">=0.33.0" + +[package.extras] +dev = ["black", "flake8", "flake8-comprehensions", "isort (>=5.0.0)", "keyring", "pre-commit", "pyjwt", "passlib", "fastapi"] +test = ["pytest (>=4.0.0)", "pytest-cov", "pytest-mock", "pytest-asyncio", "tox"] + [[package]] category = "dev" description = "the modular source code checker: pep8 pyflakes and co" @@ -976,7 +991,7 @@ python-versions = ">=3.6.1" version = "8.1" [metadata] -content-hash = "341ea53a3fbee8eaaf84587c96b10322ee991af9a038f5226175e8eb7e934070" +content-hash = "b3274fcf27c098759522090761c8df3eba6a72bc7a1285e4a1b437e1c40495fa" lock-version = "1.0" python-versions = "^3.8" @@ -1096,6 +1111,10 @@ fastapi = [ {file = "fastapi-0.61.0-py3-none-any.whl", hash = "sha256:29c12dd0d4ac825d13c2db4762d2863281c18085ae521825829182e977fd25ac"}, {file = "fastapi-0.61.0.tar.gz", hash = "sha256:d0b3f629f8d165a21ee082bf31e1697c391c1cdf940304408614b5b7c59d1fb3"}, ] +fastapi-permissions = [ + {file = "fastapi_permissions-0.2.6-py3-none-any.whl", hash = "sha256:230274ca28ff392c6e430344be4fb47941155fbf9974f4a6be057582ea3a6c22"}, + {file = "fastapi_permissions-0.2.6.tar.gz", hash = "sha256:0f852a9a501be288b12177ef9753ec45ea426d487a3c29a5f1e805463aaa3926"}, +] flake8 = [ {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, diff --git a/pyproject.toml b/pyproject.toml index bffb803..217372b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ python-rocksdb = "^0.7.0" devtools = {extras = ["pygments"], version = "^0.6"} pyjwt = "^1.7.1" passlib = {extras = ["bcrypt"], version = "^1.7.2"} +fastapi_permissions = "^0.2.6" [tool.poetry.dev-dependencies] pytest = "^5.2" diff --git a/snippets/compass/compass/services/auth/__init__.py b/snippets/compass/compass/services/auth/__init__.py index 9182009..e69de29 100644 --- a/snippets/compass/compass/services/auth/__init__.py +++ b/snippets/compass/compass/services/auth/__init__.py @@ -1,3 +0,0 @@ -from sowba.core.utils import get_service_router - -router = get_service_router("users") diff --git a/snippets/compass/compass/services/auth/acls.py b/snippets/compass/compass/services/auth/acls.py index 883a775..28b98ca 100644 --- a/snippets/compass/compass/services/auth/acls.py +++ b/snippets/compass/compass/services/auth/acls.py @@ -1,12 +1,4 @@ -from typing import List, Callable -import dataclasses as dcls -from collections import namedtuple -from fastapi_permissions import Allow, Authenticated -from pydantic import BaseModel - - -class Acl(namedtuple("Acl", ("access", "principal", "permission"))): - __slots__ = () +from sowba.security.acl import Acl class CreateItemAcl: @@ -22,18 +14,4 @@ class AclPolicy: create: CreateItemAcl = None -class BaseResourceAcl(BaseModel): - def __acl__(self): - assert self.owner is not None, "Owner must be define." - return [ - Acl(Allow, "role:user", "create"), - Acl(Allow, "role:admin", "create"), - Acl(Allow, Authenticated, "view"), - Acl(Allow, "role:admin", "update"), - Acl(Allow, f"user:{self.owner}", "update"), - Acl(Allow, "role:admin", "delete"), - Acl(Allow, f"user:{self.owner}", "delete"), - ] - - default_acl_policy = AclPolicy(create=CreateItemAcl) diff --git a/snippets/compass/compass/services/auth/model.py b/snippets/compass/compass/services/auth/model.py index cada339..fa33ff6 100644 --- a/snippets/compass/compass/services/auth/model.py +++ b/snippets/compass/compass/services/auth/model.py @@ -1,13 +1,13 @@ from typing import List from datetime import datetime from contextvars import ContextVar -from sowba.storage.utils import init_db, get_db from pydantic import BaseModel, Field, SecretStr, EmailStr from fastapi_permissions import Allow +from sowba.security import roles class UserPrincipals(BaseModel): - principals: List[str] = ["role:user"] + principals: List[str] = [roles.user] class Updateprincipals(BaseModel): @@ -56,9 +56,9 @@ class TokenData(BaseModel): class UserListAcl: - __acl__ = [(Allow, "role:admin", "view")] + __acl__ = [(Allow, roles.admin, "view")] class UserprincipalsAcl: - __acl__ = [(Allow, "role:admin", "update")] + __acl__ = [(Allow, roles.admin, "update")] diff --git a/snippets/compass/compass/services/member/acls.py b/snippets/compass/compass/services/auth/utils.py similarity index 100% rename from snippets/compass/compass/services/member/acls.py rename to snippets/compass/compass/services/auth/utils.py diff --git a/snippets/compass/config.json b/snippets/compass/config.json index 4714ac4..09970be 100644 --- a/snippets/compass/config.json +++ b/snippets/compass/config.json @@ -8,14 +8,6 @@ "model": "compass.services.items.model.Items", "connector": "rocksdb" } - }, - { - "name": "member", - "status": "enable", - "storage": { - "model": "compass.services.member.model.Member", - "connector": "rocksdb" - } } ], "storages": { @@ -41,17 +33,6 @@ } ] }, - "auth": { - "admin": { - "username": "admin", - "email": "admin@sowba.com" - }, - "security": { - "algorithm": "HS256", - "access_token_expire_minutes": 30, - "auth_path": "/token" - } - }, "asgi": { "host": "0.0.0.0", "port": 8000, @@ -71,5 +52,13 @@ "allow_methods": [ "*" ] + }, + "auth": { + "storage": { + "model": "compass.services.auth.model.User", + "connector": "rocksdb" + }, + "algorithm": "HS256", + "access_token_expire_minutes": 30 } } diff --git a/snippets/compass/poetry.lock b/snippets/compass/poetry.lock new file mode 100644 index 0000000..0c7c833 --- /dev/null +++ b/snippets/compass/poetry.lock @@ -0,0 +1,202 @@ +[[package]] +category = "dev" +description = "Foreign Function Interface for Python calling C code." +name = "cffi" +optional = false +python-versions = "*" +version = "1.14.3" + +[package.dependencies] +pycparser = "*" + +[[package]] +category = "dev" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +name = "cryptography" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "3.1" + +[package.dependencies] +cffi = ">=1.8,<1.11.3 || >1.11.3" +six = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0,<3.1.0 || >3.1.0,<3.1.1 || >3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] + +[[package]] +category = "dev" +description = "ECDSA cryptographic signature library (pure python)" +name = "ecdsa" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.14.1" + +[package.dependencies] +six = "*" + +[[package]] +category = "dev" +description = "ASN.1 types and codecs" +name = "pyasn1" +optional = false +python-versions = "*" +version = "0.4.8" + +[[package]] +category = "dev" +description = "C parser in Python" +name = "pycparser" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.20" + +[[package]] +category = "dev" +description = "JOSE implementation in Python" +name = "python-jose" +optional = false +python-versions = "*" +version = "3.2.0" + +[package.dependencies] +ecdsa = "<0.15" +pyasn1 = "*" +rsa = "*" +six = "<2.0" + +[package.dependencies.cryptography] +optional = true +version = "*" + +[package.extras] +cryptography = ["cryptography"] +pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"] +pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] + +[[package]] +category = "dev" +description = "Pure-Python RSA implementation" +name = "rsa" +optional = false +python-versions = ">=3.5, <4" +version = "4.6" + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +category = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[metadata] +content-hash = "cdd94c5370283fcc33e1f4411d343598027da76519d8a41e33f71f2ceaf481c1" +lock-version = "1.0" +python-versions = "^3.8" + +[metadata.files] +cffi = [ + {file = "cffi-1.14.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:485d029815771b9fe4fa7e1c304352fe57df6939afe835dfd0182c7c13d5e92e"}, + {file = "cffi-1.14.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c"}, + {file = "cffi-1.14.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730"}, + {file = "cffi-1.14.3-cp27-cp27m-win32.whl", hash = "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d"}, + {file = "cffi-1.14.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05"}, + {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b"}, + {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171"}, + {file = "cffi-1.14.3-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:52bf29af05344c95136df71716bb60508bbd217691697b4307dcae681612db9f"}, + {file = "cffi-1.14.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f"}, + {file = "cffi-1.14.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4"}, + {file = "cffi-1.14.3-cp35-cp35m-win32.whl", hash = "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d"}, + {file = "cffi-1.14.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d"}, + {file = "cffi-1.14.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c687778dda01832555e0af205375d649fa47afeaeeb50a201711f9a9573323b8"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537"}, + {file = "cffi-1.14.3-cp36-cp36m-win32.whl", hash = "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0"}, + {file = "cffi-1.14.3-cp36-cp36m-win_amd64.whl", hash = "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e"}, + {file = "cffi-1.14.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:03d3d238cc6c636a01cf55b9b2e1b6531a7f2f4103fabb5a744231582e68ecc7"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394"}, + {file = "cffi-1.14.3-cp37-cp37m-win32.whl", hash = "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc"}, + {file = "cffi-1.14.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869"}, + {file = "cffi-1.14.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a33558fdbee3df370399fe1712d72464ce39c66436270f3664c03f94971aff"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9"}, + {file = "cffi-1.14.3-cp38-cp38-win32.whl", hash = "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522"}, + {file = "cffi-1.14.3-cp38-cp38-win_amd64.whl", hash = "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15"}, + {file = "cffi-1.14.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d9a7dc7cf8b1101af2602fe238911bcc1ac36d239e0a577831f5dac993856e9"}, + {file = "cffi-1.14.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d"}, + {file = "cffi-1.14.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c"}, + {file = "cffi-1.14.3-cp39-cp39-win32.whl", hash = "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b"}, + {file = "cffi-1.14.3-cp39-cp39-win_amd64.whl", hash = "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3"}, + {file = "cffi-1.14.3.tar.gz", hash = "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"}, +] +cryptography = [ + {file = "cryptography-3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f"}, + {file = "cryptography-3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0"}, + {file = "cryptography-3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36"}, + {file = "cryptography-3.1-cp27-cp27m-win32.whl", hash = "sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a"}, + {file = "cryptography-3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791"}, + {file = "cryptography-3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761"}, + {file = "cryptography-3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e"}, + {file = "cryptography-3.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8"}, + {file = "cryptography-3.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c"}, + {file = "cryptography-3.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f"}, + {file = "cryptography-3.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237"}, + {file = "cryptography-3.1-cp35-cp35m-win32.whl", hash = "sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716"}, + {file = "cryptography-3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695"}, + {file = "cryptography-3.1-cp36-abi3-win32.whl", hash = "sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af"}, + {file = "cryptography-3.1-cp36-abi3-win_amd64.whl", hash = "sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618"}, + {file = "cryptography-3.1-cp36-cp36m-win32.whl", hash = "sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1"}, + {file = "cryptography-3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c"}, + {file = "cryptography-3.1-cp37-cp37m-win32.whl", hash = "sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32"}, + {file = "cryptography-3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed"}, + {file = "cryptography-3.1-cp38-cp38-win32.whl", hash = "sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67"}, + {file = "cryptography-3.1-cp38-cp38-win_amd64.whl", hash = "sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10"}, + {file = "cryptography-3.1.tar.gz", hash = "sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08"}, +] +ecdsa = [ + {file = "ecdsa-0.14.1-py2.py3-none-any.whl", hash = "sha256:e108a5fe92c67639abae3260e43561af914e7fd0d27bae6d2ec1312ae7934dfe"}, + {file = "ecdsa-0.14.1.tar.gz", hash = "sha256:64c613005f13efec6541bb0a33290d0d03c27abab5f15fbab20fb0ee162bdd8e"}, +] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +python-jose = [ + {file = "python-jose-3.2.0.tar.gz", hash = "sha256:4e4192402e100b5fb09de5a8ea6bcc39c36ad4526341c123d401e2561720335b"}, + {file = "python_jose-3.2.0-py2.py3-none-any.whl", hash = "sha256:67d7dfff599df676b04a996520d9be90d6cdb7e6dd10b4c7cacc0c3e2e92f2be"}, +] +rsa = [ + {file = "rsa-4.6-py3-none-any.whl", hash = "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"}, + {file = "rsa-4.6.tar.gz", hash = "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] diff --git a/snippets/compass/pyproject.toml b/snippets/compass/pyproject.toml index 6f09955..fb67f41 100644 --- a/snippets/compass/pyproject.toml +++ b/snippets/compass/pyproject.toml @@ -9,6 +9,7 @@ license = "MIT" python = "^3.8" [tool.poetry.dev-dependencies] +python-jose = {extras = ["cryptography"], version = "^3.2.0"} [tool.poetry.scripts] diff --git a/snippets/netizen/config.json b/snippets/netizen/config.json new file mode 100644 index 0000000..e171753 --- /dev/null +++ b/snippets/netizen/config.json @@ -0,0 +1,72 @@ +{ + "name": "netizen", + "services": [ + { + "name": "items", + "status": "enable", + "storage": { + "model": "netizen.services.items.model.Items", + "connector": "rocksdb" + } + }, + { + "name": "members", + "status": "enable", + "storage": { + "model": "netizen.services.members.model.Members", + "connector": "rocksdb" + } + } + ], + "storages": { + "default": "rocksdb", + "connectors": [ + { + "name": "rocksdb", + "factory": "sowba.storage.rocks_db.RocksDBStorage", + "configuration": {}, + "settings": {} + }, + { + "name": "mongodb", + "factory": null, + "configuration": {}, + "settings": {} + }, + { + "name": "couchbase", + "factory": null, + "configuration": {}, + "settings": {} + } + ] + }, + "asgi": { + "host": "0.0.0.0", + "port": 8000, + "debug": false, + "reload": false, + "timeout_keep_alive": 20, + "proxy_headers": true, + "log_level": "info" + }, + "cors": { + "allow_origins": [ + "*" + ], + "allow_headers": [ + "*" + ], + "allow_methods": [ + "*" + ] + }, + "auth": { + "storage": { + "model": "netizen.services.auth.model.User", + "connector": "rocksdb" + }, + "algorithm": "HS256", + "access_token_expire_minutes": 30 + } +} diff --git a/snippets/netizen/netizen/cli.py b/snippets/netizen/netizen/cli.py new file mode 100644 index 0000000..bcc8fc7 --- /dev/null +++ b/snippets/netizen/netizen/cli.py @@ -0,0 +1,17 @@ +import typer + +app = typer.Typer() + + +@app.command() +def greetings( + name: str = typer.Argument(...), formal: bool = typer.Option(False) +): + if formal: + typer.echo(f"Hello {name}!") + else: + typer.echo(f"Hey {name}!") + + +if __name__ == "__main__": + app() diff --git a/snippets/netizen/netizen/services/__init__.py b/snippets/netizen/netizen/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/snippets/netizen/netizen/services/auth/__init__.py b/snippets/netizen/netizen/services/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/snippets/netizen/netizen/services/auth/acls.py b/snippets/netizen/netizen/services/auth/acls.py new file mode 100644 index 0000000..28b98ca --- /dev/null +++ b/snippets/netizen/netizen/services/auth/acls.py @@ -0,0 +1,17 @@ +from sowba.security.acl import Acl + + +class CreateItemAcl: + __acl__ = [ + Acl(Allow, "role:user", "create"), + Acl(Allow, "role:admin", "create"), + ] + + +@dcls.dataclass +class AclPolicy: + get: Callable = None + create: CreateItemAcl = None + + +default_acl_policy = AclPolicy(create=CreateItemAcl) diff --git a/snippets/netizen/netizen/services/auth/endpoints.py b/snippets/netizen/netizen/services/auth/endpoints.py new file mode 100644 index 0000000..d4b2217 --- /dev/null +++ b/snippets/netizen/netizen/services/auth/endpoints.py @@ -0,0 +1,134 @@ +import asyncio +from fastapi import APIRouter +from datetime import timedelta +from fastapi import Depends, HTTPException +from fastapi.security import OAuth2PasswordRequestForm + +from sowba.security.model import ( + Token, + User, + UserSignup, + UserInDB, + UpdatePasswd, + UserData, + UserListAcl, + Updateprincipals, + UserprincipalsAcl, + USER_DB, +) + +from sowba.security.utils import ( + create_access_token, + authenticate_user, + get_active_principals, + ACCESS_TOKEN_EXPIRE_MINUTES, + get_current_user, + principals_validator, + pwd_context, +) + +from fastapi_permissions import configure_permissions + + +router = APIRouter() +Permission = configure_permissions(get_active_principals) + + +@router.get("/", tags=["Security"]) +async def users( + acl: UserListAcl = Permission("view", UserListAcl), + current_user: User = Depends(get_current_user), +): + if asyncio.iscoroutinefunction(USER_DB.get_all): + return await USER_DB.get_all() + return USER_DB.get_all() + + +@router.post("/@update_principals/{uid}", tags=["Security"]) +async def update_principals( + uid: str, + principals: Updateprincipals, + acl: UserprincipalsAcl = Permission("update", UserprincipalsAcl), + current_user: User = Depends(get_current_user), +): + if asyncio.iscoroutinefunction(USER_DB.get): + user = await USER_DB.get(uid) + else: + user = USER_DB.get(uid) + + user = user["item"] + for action in principals.fields: + principals_validator(vars(principals)[action]) + if action == "add": + user.principals = list(set(user.principals + principals.add)) + elif action == "delete": + user.principals = list( + set(user.principals) - set(principals.delete) + ) + + if asyncio.iscoroutinefunction(USER_DB.store): + response = await USER_DB.store(oid=user.email, obj=user) + else: + response = USER_DB.store(oid=user.email, obj=user) + + response["item"] = User(**user.dict()) + return response + + +@router.post("/token", response_model=Token, tags=["Security"]) +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends(), +): + user = await authenticate_user( + USER_DB, form_data.username, form_data.password + ) + if not user: + raise HTTPException( + status_code=400, detail="Incorrect username or password" + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.email}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@router.get("/@me", response_model=User, tags=["Security"]) +async def me(current_user: User = Depends(get_current_user)): + return current_user + + +@router.post("/@update_passwd", tags=["Security"]) +async def update_passwd( + password: UpdatePasswd, current_user: User = Depends(get_current_user) +): + return password + + +@router.post("/@update_user", tags=["Security"]) +async def update_user( + data: UserData, current_user: User = Depends(get_current_user) +): + return data + + +@router.post("/@signup", tags=["Security"]) +async def signup(new_user: UserSignup): + if new_user.password != new_user.confirm_password: + raise HTTPException( + status_code=412, detail="Invalid confirmation password!" + ) + new_user = UserInDB( + **{ + **new_user.dict(), + "hashed_password": pwd_context.hash( + new_user.password.get_secret_value() + ), + } + ) + new_user.principals = [ + *{*new_user.principals, f"user:{new_user.email}", "role:user"} + ] + response = USER_DB.store(oid=new_user.email, obj=new_user) + response["item"] = User(**new_user.dict()) + return response diff --git a/snippets/netizen/netizen/services/auth/model.py b/snippets/netizen/netizen/services/auth/model.py new file mode 100644 index 0000000..fa33ff6 --- /dev/null +++ b/snippets/netizen/netizen/services/auth/model.py @@ -0,0 +1,64 @@ +from typing import List +from datetime import datetime +from contextvars import ContextVar +from pydantic import BaseModel, Field, SecretStr, EmailStr +from fastapi_permissions import Allow +from sowba.security import roles + + +class UserPrincipals(BaseModel): + principals: List[str] = [roles.user] + + +class Updateprincipals(BaseModel): + add: List[str] = [] + delete: List[str] = [] + + +class UserData(BaseModel): + first_name: str + last_name: str + + +class User(UserData, UserPrincipals): + username: str + email: EmailStr + + @property + def full_name(self): + return f"{self.first_name} {self.last_name}" + + +class UserSignup(User): + password: SecretStr + confirm_password: SecretStr + + +class UserInDB(User): + hashed_password: str + created_on: datetime = Field(default_factory=datetime.utcnow) + updateed_on: datetime = Field(default_factory=datetime.utcnow) + + +class UpdatePasswd(BaseModel): + current_password: SecretStr + password: SecretStr + confirm_password: SecretStr + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str = None + + +class UserListAcl: + __acl__ = [(Allow, roles.admin, "view")] + + +class UserprincipalsAcl: + __acl__ = [(Allow, roles.admin, "update")] + diff --git a/snippets/netizen/netizen/services/auth/roles.py b/snippets/netizen/netizen/services/auth/roles.py new file mode 100644 index 0000000..e69de29 diff --git a/snippets/netizen/netizen/services/auth/utils.py b/snippets/netizen/netizen/services/auth/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/snippets/netizen/netizen/services/items/__init__.py b/snippets/netizen/netizen/services/items/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/snippets/netizen/netizen/services/items/acls.py b/snippets/netizen/netizen/services/items/acls.py new file mode 100644 index 0000000..e69de29 diff --git a/snippets/compass/compass/services/member/endpoints/__init__.py b/snippets/netizen/netizen/services/items/endpoints/__init__.py similarity index 100% rename from snippets/compass/compass/services/member/endpoints/__init__.py rename to snippets/netizen/netizen/services/items/endpoints/__init__.py diff --git a/snippets/compass/compass/services/member/endpoints/utils.py b/snippets/netizen/netizen/services/items/endpoints/utils.py similarity index 83% rename from snippets/compass/compass/services/member/endpoints/utils.py rename to snippets/netizen/netizen/services/items/endpoints/utils.py index d989761..493ef40 100644 --- a/snippets/compass/compass/services/member/endpoints/utils.py +++ b/snippets/netizen/netizen/services/items/endpoints/utils.py @@ -1,7 +1,7 @@ from sowba.registry import get as get_registry -router = get_registry.service("member") +router = get_registry.service("items") @router.get("/api/@ping") diff --git a/snippets/compass/compass/services/member/model.py b/snippets/netizen/netizen/services/items/model.py similarity index 51% rename from snippets/compass/compass/services/member/model.py rename to snippets/netizen/netizen/services/items/model.py index 7259fa3..5767c50 100644 --- a/snippets/compass/compass/services/member/model.py +++ b/snippets/netizen/netizen/services/items/model.py @@ -1,5 +1,5 @@ from sowba.core.model import SBaseModel -class Member(SBaseModel): - ... +class Items(SBaseModel): + name: str diff --git a/snippets/netizen/netizen/services/members/__init__.py b/snippets/netizen/netizen/services/members/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/snippets/netizen/netizen/services/members/acls.py b/snippets/netizen/netizen/services/members/acls.py new file mode 100644 index 0000000..e69de29 diff --git a/snippets/netizen/netizen/services/members/endpoints/__init__.py b/snippets/netizen/netizen/services/members/endpoints/__init__.py new file mode 100644 index 0000000..50820d5 --- /dev/null +++ b/snippets/netizen/netizen/services/members/endpoints/__init__.py @@ -0,0 +1 @@ +from .utils import ping diff --git a/snippets/netizen/netizen/services/members/endpoints/utils.py b/snippets/netizen/netizen/services/members/endpoints/utils.py new file mode 100644 index 0000000..4f7be1a --- /dev/null +++ b/snippets/netizen/netizen/services/members/endpoints/utils.py @@ -0,0 +1,14 @@ +from sowba.registry import get as get_registry + + +router = get_registry.service("members") + + +@router.get("/api/@ping") +async def ping(): + return {"response": "OK"} + + +@router.get("/api/@info") +async def info(): + return {"info": "OK"} diff --git a/snippets/netizen/netizen/services/members/model.py b/snippets/netizen/netizen/services/members/model.py new file mode 100644 index 0000000..be7d153 --- /dev/null +++ b/snippets/netizen/netizen/services/members/model.py @@ -0,0 +1,6 @@ +from sowba.core.model import SBaseModel + + +class Members(SBaseModel): + name: str + age: int diff --git a/snippets/netizen/pyproject.toml b/snippets/netizen/pyproject.toml new file mode 100644 index 0000000..42b17ec --- /dev/null +++ b/snippets/netizen/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "netizen" +version = "0.1.0" +description = "" +authors = ["Developer "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.8" + +[tool.poetry.dev-dependencies] + + +[tool.poetry.scripts] +sowba = 'netizen.cli:app' + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/sowba/cli/__init__.py b/sowba/cli/__init__.py index 505d400..f3518eb 100644 --- a/sowba/cli/__init__.py +++ b/sowba/cli/__init__.py @@ -16,7 +16,7 @@ from sowba.registry import get as get_registry from sowba.registry import add as add_registry from sowba.core import path -from sowba.cli import config, service +from sowba.cli import config, service, auth from sowba.settings.model import StorageName, ServiceStatus from sowba.cli.utils import ( @@ -27,6 +27,7 @@ make_app, run_app, load_service_endpoints, + load_sevcurity, ) @@ -35,14 +36,13 @@ app = typer.Typer() app.add_typer(config.app, name="config") app.add_typer(service.app, name="service") +app.add_typer(auth.app, name="auth") @app.callback() def app_callback( ctx: typer.Context, config_file: Path = typer.Option("./config.json", "--config-file"), - admin_password: str = typer.Option(None, "--admin-passwd"), - secret_key: str = typer.Option(None, "--secret-key"), ): if ctx.invoked_subcommand == "create": typer.echo("Creating new project") @@ -52,12 +52,6 @@ def app_callback( typer.echo(f"Config file [{config_file}] not found.") raise typer.Abort() - if admin_password: - os.environ["SOWBA_ENV_ADMIN_USER_PASSWORD"] = admin_password - - if secret_key: - os.environ["SOWBA_ENV_SECURITY_SECRET_KEY"] = secret_key - try: add_registry.app_settings(load_settings(config_file)) except ValidationError as err: @@ -100,6 +94,9 @@ def run(storage: StorageName = typer.Option(None, "--settings-storage")): settings.services[i].storage.connector = storage app = make_app(settings) + if getattr(settings, "auth", None): + load_sevcurity(app) + for srv in settings.services: if srv.status == ServiceStatus.disable: continue diff --git a/sowba/cli/auth.py b/sowba/cli/auth.py new file mode 100644 index 0000000..8513124 --- /dev/null +++ b/sowba/cli/auth.py @@ -0,0 +1,172 @@ +""" +sowba service [name] ls +sowba service [name] enable +sowba service [name] disable + +sowba service add [name] --settings-storage=rocksdb +sowba service run [name] --settings-storage=rocksdb +""" +import json + +import devtools +import typer +from enum import Enum +from typing import List +from pydantic import PyObject, PyObjectError, EmailStr, EmailError +from sowba.cli.utils import ( + load_raw_settings, + create_service, + update_service_status, + make_service_storage, + make_service, + make_app, + run_app, + load_service_endpoints, +) +from sowba.core import path +from sowba.settings.model import ConnectorConfig +from sowba.settings.model import ServiceStorage +from sowba.settings.model import ServiceStatus +from sowba.settings.model import StorageName +from sowba.registry import get as get_registry +from sowba.registry import add as add_registry +from sowba.settings.model import JwtAlgorithm +from sowba.security import roles +from sowba.settings.model import AppSettings +from sowba.cli.utils import load_raw_settings + + +from cookiecutter.main import cookiecutter +from cookiecutter.exceptions import OutputDirExistsException + + +app = typer.Typer() + + +class UserType(str, Enum): + user: str = "user" + admin: str = "admin" + + +def validate_email(email): + try: + email = EmailStr.validate(email) + except EmailError as err: + raise typer.BadParameter(str(err)) + return email + + +def add_auth_service( + settings: AppSettings, + secret_key: str = None, + access_token_ttl: int = 30, + storage: StorageName = StorageName.rocksdb, + algorithm: JwtAlgorithm = JwtAlgorithm.hs256, +): + raw_settings = load_raw_settings(settings.file) + config_file = raw_settings.pop("file") + auth = { + "storage": { + "model": f"{settings.name}.services.auth.model.User", + "connector": storage, + }, + "algorithm": JwtAlgorithm.hs256, + "access_token_expire_minutes": access_token_ttl or 30, + } + if secret_key: + auth["secret_key"] = secret_key + raw_settings["auth"] = auth + + try: + cookiecutter( + f"{path.template.auth}", + output_dir=f"{path.cwd()}/{settings.name}/services", + no_input=True, + extra_context={ + "auth": "auth", + "storage": storage, + "algorithm": JwtAlgorithm.hs256, + "access_token_expire_minutes": access_token_ttl or 30, + }, + ) + print(json.dumps(raw_settings, indent=4), file=open(config_file, "wt")) + except OutputDirExistsException: + typer.echo("Eroor: Auth alredy added!") + raise typer.Abort() + return AppSettings(file=config_file, **raw_settings) + + +def add_user(storage, username, emali, password, principals): + breakpoint() + storage.close() + return + ... + + +def load_auth_service(settings: AppSettings): + ... + + +def load_auth_storage(storage: StorageName, settings: AppSettings): + for sconf in settings.storages.connectors: + if sconf.name == storage: + stg = sconf.factory() + stg.configure( + settings.auth.storage.model, + **getattr(sconf, "configuration", ConnectorConfig()).dict(), + ) + stg.settings(**getattr(sconf, "settings", dict())) + return stg + else: + return None + # conf: ServiceStorage = settings.auth.storage + # storage = make_auth_storage(srv.name, settings) + # add_registry.storage("auth", storage) + ... + + +@app.command() +def init( + secret_key: str = typer.Option(None, "--secret-key"), + algorithm: JwtAlgorithm = typer.Option(JwtAlgorithm.hs256, "--jwt-algo"), + access_token_ttl: int = typer.Option(30, "--token-ttl"), + root_emali: str = typer.Option( + "root@sowba.com", + prompt=True, + confirmation_prompt=True, + callback=validate_email, + ), + password: str = typer.Option( + "ChangeMe", prompt=True, confirmation_prompt=True, hide_input=True + ), + storage: StorageName = typer.Option(StorageName.rocksdb), +): + username = "root" + principals = [f"{roles.root}"] + + settings = get_registry.app_settings() + settings = add_auth_service( + settings, + storage=storage, + algorithm=algorithm, + secret_key=secret_key, + access_token_ttl=access_token_ttl, + ) + add_user( + load_auth_storage(storage, settings), + username, + root_emali, + password, + principals, + ) + + +@app.command() +def add(user: UserType = typer.Argument(UserType.user)): + # settings = get_registry.app_settings() + breakpoint() + return + + +if __name__ == "__main__": + app() diff --git a/sowba/cli/utils.py b/sowba/cli/utils.py index 8822516..37347f7 100644 --- a/sowba/cli/utils.py +++ b/sowba/cli/utils.py @@ -22,6 +22,8 @@ from pydantic import FilePath from pydantic.parse import load_file +from sowba.core.model import SBaseModel + def create_app( name, /, output="./", storage: StorageName = "rocksdb", template="default" @@ -124,3 +126,7 @@ def load_settings( config: FilePath = pathlib.Path("./config.json"), ) -> AppSettings: return AppSettings(**load_raw_settings(config)) + + +def load_sevcurity(app: SApp): + return diff --git a/sowba/core/path.py b/sowba/core/path.py index 7ee8680..78bce3d 100644 --- a/sowba/core/path.py +++ b/sowba/core/path.py @@ -23,6 +23,10 @@ class Template: def app(self) -> pathlib.Path: return pathlib.Path(f"{cookiecutter()}/app") + @property + def auth(self) -> pathlib.Path: + return pathlib.Path(f"{cookiecutter()}/auth") + @property def service(self) -> pathlib.Path: return pathlib.Path(f"{cookiecutter()}/service") diff --git a/sowba/security/acl.py b/sowba/security/acl.py index 75980fd..1d1740e 100644 --- a/sowba/security/acl.py +++ b/sowba/security/acl.py @@ -16,6 +16,7 @@ class CreateItemAcl: __acl__ = [ Acl(Allow, roles.user, "create"), Acl(Allow, roles.admin, "create"), + Acl(Allow, roles.root, "create"), ] diff --git a/sowba/security/roles.py b/sowba/security/roles.py index b9939b0..e708a89 100644 --- a/sowba/security/roles.py +++ b/sowba/security/roles.py @@ -1,3 +1,21 @@ -user: str = "role:sowba.User" -root: str = "role:sowba.Root" -admin: str = "role:sowba.Admin" +class Role(str): + """ + Role("user") == "role:sowba.User" + """ + + def __init__(self, role: str): + self.role = role + + def __str__(self): + return f"role:sowba.{self.role.capitalize()}" + + def __repr__(self): + return f"role:sowba.{self.role.capitalize()}" + + def __eq__(self, value): + return value == f"role:sowba.{self.role.capitalize()}" + + +user = Role("user") +root = Role("root") +admin = Role("admin") diff --git a/sowba/settings/model.py b/sowba/settings/model.py index b345a83..344aecf 100644 --- a/sowba/settings/model.py +++ b/sowba/settings/model.py @@ -64,22 +64,6 @@ class AdminUser(BaseSettings): ) -class Security(BaseSettings): - """ - export SOWBA_ENV_SECURITY_SECRET_KEY="$(openssl rand -hex 32)" - """ - - secret_key: str = Field("ChangeMe", env="SOWBA_ENV_SECURITY_SECRET_KEY") - auth_path: str - algorithm: JwtAlgorithm - access_token_expire_minutes: int - - -class Auth(BaseSettings): - admin: AdminUser - security: Security - - class Redis(BaseModel): dsn: RedisDsn settings: Dict = Field(default_factory=dict) @@ -120,6 +104,17 @@ class Service(BaseModel): settings: Optional[Dict] +class Auth(BaseSettings): + """ + export SOWBA_ENV_SECURITY_SECRET_KEY="$(openssl rand -hex 32)" + """ + + algorithm: JwtAlgorithm + access_token_expire_minutes: int + storage: ServiceStorage + secret_key: str = Field("ChangeMe", env="SOWBA_ENV_SECURITY_SECRET_KEY") + + class AppSettings(BaseSettings): name: str file: FilePath