pull server upstream at 55d3fb7e
(v0.11.0)
commit
30189cd485
|
@ -14,3 +14,7 @@ __pycache__
|
|||
|
||||
/etebase_server_settings.py
|
||||
/secret.txt
|
||||
|
||||
/build
|
||||
/dist
|
||||
/*.egg-info
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
# Changelog
|
||||
|
||||
## Version 0.11.0
|
||||
- Update deps for Python 3.11
|
||||
|
||||
## Version 0.10.0
|
||||
- Replace the deprecated aioredis with redis-py
|
||||
- Optimize how we fetch the current (latest) revision of items
|
||||
|
||||
## Version 0.9.1
|
||||
- Update pinned Django version (only matters if using `requirements.txt`).
|
||||
|
||||
## Version 0.9.0
|
||||
- Add LDAP support for checking the validity of a username
|
||||
- Allow specifying engine-specific database options
|
||||
- Fix crash on shutdown when redis isn't used
|
||||
- Reorganize the code to be a valid Python package
|
||||
|
||||
## Version 0.8.3
|
||||
- Fix compatibility with latest fastapi
|
||||
|
||||
|
|
|
@ -63,6 +63,12 @@ Now you can initialise our django app.
|
|||
./manage.py migrate
|
||||
```
|
||||
|
||||
Create static files:
|
||||
|
||||
```
|
||||
./manage.py collectstatic
|
||||
```
|
||||
|
||||
And you are done! You can now run the debug server just to see everything works as expected by running:
|
||||
|
||||
```
|
||||
|
@ -151,7 +157,7 @@ Instead of having to create Django users manually when signup up Etebase users,
|
|||
For example, this makes sense when putting an Etebase server in production.
|
||||
However, this does come with the added risk that everybody with access to your server will be able to sign up.
|
||||
|
||||
In order to set it up, comment out the line `ETEBASE_CREATE_USER_FUNC = "django_etebase.utils.create_user_blocked"` in `server/settings.py` and restart your Etebase server.
|
||||
In order to set it up, comment out the line `ETEBASE_CREATE_USER_FUNC = "etebase_server.django.utils.create_user_blocked"` in `server/settings.py` and restart your Etebase server.
|
||||
|
||||
# License
|
||||
|
||||
|
@ -177,3 +183,5 @@ Become a financial contributor and help us sustain our community!
|
|||
|
||||
[](https://github.com/ilovept)
|
||||
[](https://github.com/ryanleesipes)
|
||||
[](https://github.com/DanielG)
|
||||
[](https://github.com/Kanaye)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
user_signed_up = Signal(providing_args=["request", "user"])
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.9.0-alpine
|
||||
FROM python:3.11.0-alpine
|
||||
|
||||
ARG ETESYNC_VERSION
|
||||
|
||||
|
|
|
@ -9,4 +9,4 @@ allowed_host1 = *
|
|||
|
||||
[database]
|
||||
engine = django.db.backends.sqlite3
|
||||
name = /db.sqlite3
|
||||
name = /data/db.sqlite3
|
||||
|
|
|
@ -18,3 +18,18 @@ allowed_host1 = example.com
|
|||
[database]
|
||||
engine = django.db.backends.sqlite3
|
||||
name = db.sqlite3
|
||||
|
||||
[database-options]
|
||||
; Add engine-specific options here, such as postgresql parameter key words
|
||||
|
||||
;[ldap]
|
||||
;server = <The URL to your LDAP server>
|
||||
;search_base = <Your search base>
|
||||
;filter = <Your LDAP filter query. '%%s' will be substituted for the username>
|
||||
; In case a cache TTL of 1 hour is too short for you, set `cache_ttl` to the preferred
|
||||
; amount of hours a cache entry should be viewed as valid:
|
||||
;cache_ttl = 5
|
||||
;bind_dn = <Your LDAP "user" to bind as. Must be a bind user>
|
||||
; Either specify the password directly, or provide a password file
|
||||
;bind_pw = <The password to authenticate as your bind user>
|
||||
;bind_pw_file = /path/to/the/file.txt
|
||||
|
|
|
@ -7,7 +7,7 @@ django_application = get_asgi_application()
|
|||
|
||||
|
||||
def create_application():
|
||||
from etebase_fastapi.main import create_application
|
||||
from etebase_server.fastapi.main import create_application
|
||||
|
||||
app = create_application()
|
||||
|
||||
|
|
|
@ -2,4 +2,5 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class DjangoEtebaseConfig(AppConfig):
|
||||
name = "django_etebase"
|
||||
name = "etebase_server.django"
|
||||
label = "django_etebase"
|
|
@ -4,7 +4,7 @@ from django.conf import settings
|
|||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_etebase.models
|
||||
from etebase_server.django.models import chunk_directory_path
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -85,7 +85,7 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"chunkFile",
|
||||
models.FileField(max_length=150, unique=True, upload_to=django_etebase.models.chunk_directory_path),
|
||||
models.FileField(max_length=150, unique=True, upload_to=chunk_directory_path),
|
||||
),
|
||||
(
|
||||
"item",
|
|
@ -3,7 +3,7 @@
|
|||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_etebase.models
|
||||
from etebase_server.django.models import generate_stoken_uid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
|||
"uid",
|
||||
models.CharField(
|
||||
db_index=True,
|
||||
default=django_etebase.models.generate_stoken_uid,
|
||||
default=generate_stoken_uid,
|
||||
max_length=43,
|
||||
unique=True,
|
||||
validators=[
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django_etebase.models
|
||||
from etebase_server.django.models import generate_stoken_uid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -62,7 +62,7 @@ class Migration(migrations.Migration):
|
|||
name="uid",
|
||||
field=models.CharField(
|
||||
db_index=True,
|
||||
default=django_etebase.models.generate_stoken_uid,
|
||||
default=generate_stoken_uid,
|
||||
max_length=43,
|
||||
unique=True,
|
||||
validators=[
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from django.db import migrations
|
||||
|
||||
from django_etebase.models import AccessLevels
|
||||
from etebase_server.django.models import AccessLevels
|
||||
|
||||
|
||||
def change_access_level_to_int(apps, schema_editor):
|
|
@ -96,7 +96,7 @@ class CollectionItem(models.Model):
|
|||
|
||||
@cached_property
|
||||
def content(self) -> "CollectionItemRevision":
|
||||
return self.revisions.get(current=True)
|
||||
return self.revisions.filter(current=True)[0]
|
||||
|
||||
@property
|
||||
def etag(self) -> str:
|
|
@ -0,0 +1,4 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
# Provides arguments "request" and "user"
|
||||
user_signed_up = Signal()
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class TokenAuthConfig(AppConfig):
|
||||
name = "django_etebase.token_auth"
|
||||
name = "etebase_server.django.token_auth"
|
|
@ -3,7 +3,7 @@
|
|||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django_etebase.token_auth import models as token_auth_models
|
||||
from etebase_server.django.token_auth import models as token_auth_models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
|
@ -1,7 +1,7 @@
|
|||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from myauth.models import get_typed_user_model
|
||||
from etebase_server.myauth.models import get_typed_user_model
|
||||
|
||||
User = get_typed_user_model()
|
||||
|
|
@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|||
|
||||
from django.db.models import QuerySet
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from myauth.models import UserType, get_typed_user_model
|
||||
from etebase_server.myauth.models import UserType, get_typed_user_model
|
||||
|
||||
from . import app_settings
|
||||
|
|
@ -6,9 +6,9 @@ from fastapi.security import APIKeyHeader
|
|||
from django.utils import timezone
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from django_etebase import models
|
||||
from django_etebase.token_auth.models import AuthToken, get_default_expiry
|
||||
from myauth.models import UserType, get_typed_user_model
|
||||
from etebase_server.django import models
|
||||
from etebase_server.django.token_auth.models import AuthToken, get_default_expiry
|
||||
from etebase_server.myauth.models import UserType, get_typed_user_model
|
||||
from .exceptions import AuthenticationFailed
|
||||
from .utils import get_object_or_404
|
||||
from .db_hack import django_db_cleanup_decorator
|
|
@ -6,7 +6,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from django_etebase import app_settings
|
||||
from etebase_server.django import app_settings
|
||||
|
||||
from .exceptions import CustomHttpException
|
||||
from .msgpack import MsgpackResponse
|
||||
|
@ -43,7 +43,7 @@ def create_application(prefix="", middlewares=[]):
|
|||
app.include_router(websocket_router, prefix=f"{BASE_PATH}/ws", tags=["websocket"])
|
||||
|
||||
if settings.DEBUG:
|
||||
from etebase_fastapi.routers.test_reset_view import test_reset_view_router
|
||||
from .routers.test_reset_view import test_reset_view_router
|
||||
|
||||
app.include_router(test_reset_view_router, prefix=f"{BASE_PATH}/test/authentication")
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import typing as t
|
||||
import aioredis
|
||||
from redis import asyncio as aioredis
|
||||
|
||||
from django_etebase import app_settings
|
||||
from etebase_server.django import app_settings
|
||||
|
||||
|
||||
class RedisWrapper:
|
||||
|
@ -12,12 +12,11 @@ class RedisWrapper:
|
|||
|
||||
async def setup(self):
|
||||
if self.redis_uri is not None:
|
||||
self.redis = await aioredis.create_redis_pool(self.redis_uri)
|
||||
self.redis = await aioredis.from_url(self.redis_uri)
|
||||
|
||||
async def close(self):
|
||||
if self.redis is not None:
|
||||
self.redis.close()
|
||||
await self.redis.wait_closed()
|
||||
if hasattr(self, "redis"):
|
||||
await self.redis.close()
|
||||
|
||||
@property
|
||||
def is_active(self):
|
|
@ -14,12 +14,12 @@ from django.db import transaction
|
|||
from django.utils.functional import cached_property
|
||||
from fastapi import APIRouter, Depends, status, Request
|
||||
|
||||
from django_etebase import app_settings, models
|
||||
from django_etebase.token_auth.models import AuthToken
|
||||
from django_etebase.models import UserInfo
|
||||
from django_etebase.signals import user_signed_up
|
||||
from django_etebase.utils import create_user, get_user_queryset, CallbackContext
|
||||
from myauth.models import UserType, get_typed_user_model
|
||||
from etebase_server.django import app_settings, models
|
||||
from etebase_server.django.token_auth.models import AuthToken
|
||||
from etebase_server.django.models import UserInfo
|
||||
from etebase_server.django.signals import user_signed_up
|
||||
from etebase_server.django.utils import create_user, get_user_queryset, CallbackContext
|
||||
from etebase_server.myauth.models import UserType, get_typed_user_model
|
||||
from ..exceptions import AuthenticationFailed, transform_validation_error, HttpError
|
||||
from ..msgpack import MsgpackRoute
|
||||
from ..utils import BaseModel, permission_responses, msgpack_encode, msgpack_decode, get_user_username_email_kwargs
|
|
@ -7,8 +7,8 @@ from django.db import transaction, IntegrityError
|
|||
from django.db.models import Q, QuerySet
|
||||
from fastapi import APIRouter, Depends, status, Request, BackgroundTasks
|
||||
|
||||
from django_etebase import models
|
||||
from myauth.models import UserType
|
||||
from etebase_server.django import models
|
||||
from etebase_server.myauth.models import UserType
|
||||
from .authentication import get_authenticated_user
|
||||
from .websocket import get_ticket, TicketRequest, TicketOut
|
||||
from ..exceptions import HttpError, transform_validation_error, PermissionDenied, ValidationError
|
||||
|
@ -396,7 +396,7 @@ def item_create(item_model: CollectionItemIn, collection: models.Collection, val
|
|||
if not created:
|
||||
# We don't have to use select_for_update here because the unique constraint on current guards against
|
||||
# the race condition. But it's a good idea because it'll lock and wait rather than fail.
|
||||
current_revision = instance.revisions.filter(current=True).select_for_update().first()
|
||||
current_revision = instance.revisions.filter(current=True).select_for_update()[0]
|
||||
assert current_revision is not None
|
||||
current_revision.current = None
|
||||
current_revision.save()
|
|
@ -4,9 +4,9 @@ from django.db import transaction, IntegrityError
|
|||
from django.db.models import QuerySet
|
||||
from fastapi import APIRouter, Depends, status, Request
|
||||
|
||||
from django_etebase import models
|
||||
from django_etebase.utils import get_user_queryset, CallbackContext
|
||||
from myauth.models import UserType, get_typed_user_model
|
||||
from etebase_server.django import models
|
||||
from etebase_server.django.utils import get_user_queryset, CallbackContext
|
||||
from etebase_server.myauth.models import UserType, get_typed_user_model
|
||||
from .authentication import get_authenticated_user
|
||||
from ..exceptions import HttpError, PermissionDenied
|
||||
from ..msgpack import MsgpackRoute
|
|
@ -4,8 +4,8 @@ from django.db import transaction
|
|||
from django.db.models import QuerySet
|
||||
from fastapi import APIRouter, Depends, status
|
||||
|
||||
from django_etebase import models
|
||||
from myauth.models import UserType, get_typed_user_model
|
||||
from etebase_server.django import models
|
||||
from etebase_server.myauth.models import UserType, get_typed_user_model
|
||||
from .authentication import get_authenticated_user
|
||||
from ..msgpack import MsgpackRoute
|
||||
from ..utils import get_object_or_404, BaseModel, permission_responses, PERMISSIONS_READ, PERMISSIONS_READWRITE
|
|
@ -3,11 +3,11 @@ from django.db import transaction
|
|||
from django.shortcuts import get_object_or_404
|
||||
from fastapi import APIRouter, Request, status
|
||||
|
||||
from django_etebase.utils import get_user_queryset, CallbackContext
|
||||
from etebase_server.django.utils import get_user_queryset, CallbackContext
|
||||
from .authentication import SignupIn, signup_save
|
||||
from ..msgpack import MsgpackRoute
|
||||
from ..exceptions import HttpError
|
||||
from myauth.models import get_typed_user_model
|
||||
from etebase_server.myauth.models import get_typed_user_model
|
||||
|
||||
test_reset_view_router = APIRouter(route_class=MsgpackRoute, tags=["test helpers"])
|
||||
User = get_typed_user_model()
|
|
@ -1,16 +1,17 @@
|
|||
import asyncio
|
||||
import typing as t
|
||||
|
||||
import aioredis
|
||||
from redis import asyncio as aioredis
|
||||
from redis.exceptions import ConnectionError
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.db.models import QuerySet
|
||||
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, status
|
||||
import nacl.encoding
|
||||
import nacl.utils
|
||||
|
||||
from django_etebase import models
|
||||
from django_etebase.utils import CallbackContext, get_user_queryset
|
||||
from myauth.models import UserType, get_typed_user_model
|
||||
from etebase_server.django import models
|
||||
from etebase_server.django.utils import CallbackContext, get_user_queryset
|
||||
from etebase_server.myauth.models import UserType, get_typed_user_model
|
||||
|
||||
from ..dependencies import get_collection_queryset, get_item_queryset
|
||||
from ..exceptions import NotSupported
|
||||
|
@ -51,7 +52,7 @@ async def get_ticket(
|
|||
uid = nacl.encoding.URLSafeBase64Encoder.encode(nacl.utils.random(32))
|
||||
ticket_model = TicketInner(user=user.id, req=ticket_request)
|
||||
ticket_raw = msgpack_encode(ticket_model.dict())
|
||||
await redisw.redis.set(uid, ticket_raw, expire=TICKET_VALIDITY_SECONDS * 1000)
|
||||
await redisw.redis.set(uid, ticket_raw, ex=TICKET_VALIDITY_SECONDS * 1000)
|
||||
return TicketOut(ticket=uid)
|
||||
|
||||
|
||||
|
@ -103,9 +104,9 @@ async def send_item_updates(
|
|||
|
||||
async def redis_connector(websocket: WebSocket, ticket_model: TicketInner, user: UserType, stoken: t.Optional[str]):
|
||||
async def producer_handler(r: aioredis.Redis, ws: WebSocket):
|
||||
pubsub = r.pubsub()
|
||||
channel_name = f"col.{ticket_model.req.collection}"
|
||||
(channel,) = await r.psubscribe(channel_name)
|
||||
assert isinstance(channel, aioredis.Channel)
|
||||
await pubsub.subscribe(channel_name)
|
||||
|
||||
# Send missing items if we are not up to date
|
||||
queryset: QuerySet[models.Collection] = get_collection_queryset(user)
|
||||
|
@ -117,12 +118,20 @@ async def redis_connector(websocket: WebSocket, ticket_model: TicketInner, user:
|
|||
return
|
||||
await send_item_updates(websocket, collection, user, stoken)
|
||||
|
||||
async def handle_message():
|
||||
msg = await pubsub.get_message(ignore_subscribe_messages=True, timeout=20)
|
||||
message_raw = t.cast(t.Optional[t.Tuple[str, bytes]], msg)
|
||||
if message_raw:
|
||||
_, message = message_raw
|
||||
await ws.send_bytes(message)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# We wait on the websocket so we fail if web sockets fail or get data
|
||||
receive = asyncio.create_task(websocket.receive())
|
||||
done, pending = await asyncio.wait(
|
||||
{receive, channel.wait_message()}, return_when=asyncio.FIRST_COMPLETED
|
||||
{receive, handle_message()},
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
@ -131,12 +140,7 @@ async def redis_connector(websocket: WebSocket, ticket_model: TicketInner, user:
|
|||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||
return
|
||||
|
||||
message_raw = t.cast(t.Optional[t.Tuple[str, bytes]], await channel.get())
|
||||
if message_raw:
|
||||
_, message = message_raw
|
||||
await ws.send_bytes(message)
|
||||
|
||||
except aioredis.errors.ConnectionClosedError:
|
||||
except ConnectionError:
|
||||
await websocket.close(code=status.WS_1012_SERVICE_RESTART)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
|
@ -3,7 +3,7 @@ import typing as t
|
|||
from django.db.models import QuerySet
|
||||
from fastapi import status
|
||||
|
||||
from django_etebase.models import Stoken
|
||||
from etebase_server.django.models import Stoken
|
||||
|
||||
from .exceptions import HttpError
|
||||
|
|
@ -10,9 +10,9 @@ from pydantic import BaseModel as PyBaseModel
|
|||
from django.db.models import Model, QuerySet
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from django_etebase import app_settings
|
||||
from django_etebase.models import AccessLevels
|
||||
from myauth.models import UserType, get_typed_user_model
|
||||
from etebase_server.django import app_settings
|
||||
from etebase_server.django.models import AccessLevels
|
||||
from etebase_server.myauth.models import UserType, get_typed_user_model
|
||||
|
||||
from .exceptions import HttpError, HttpErrorOut
|
||||
|
|
@ -2,4 +2,5 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class MyauthConfig(AppConfig):
|
||||
name = "myauth"
|
||||
name = "etebase_server.myauth"
|
||||
label = "myauth"
|
|
@ -1,6 +1,6 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import UsernameField
|
||||
from myauth.models import get_typed_user_model
|
||||
from etebase_server.myauth.models import get_typed_user_model
|
||||
|
||||
User = get_typed_user_model()
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
|
||||
from etebase_server.django.utils import CallbackContext
|
||||
from etebase_server.myauth.models import get_typed_user_model, UserType
|
||||
from etebase_server.fastapi.dependencies import get_authenticated_user
|
||||
from etebase_server.fastapi.exceptions import PermissionDenied as FastAPIPermissionDenied
|
||||
from fastapi import Depends
|
||||
|
||||
import ldap
|
||||
|
||||
User = get_typed_user_model()
|
||||
|
||||
|
||||
def ldap_setting(name, default):
|
||||
"""Wrapper around django.conf.settings"""
|
||||
return getattr(settings, f"LDAP_{name}", default)
|
||||
|
||||
|
||||
class LDAPConnection:
|
||||
__instance__ = None
|
||||
__user_cache = {} # Username -> Valid until
|
||||
|
||||
@staticmethod
|
||||
def get_instance():
|
||||
"""To get a Singleton"""
|
||||
if not LDAPConnection.__instance__:
|
||||
return LDAPConnection()
|
||||
else:
|
||||
return LDAPConnection.__instance__
|
||||
|
||||
def __init__(self):
|
||||
# Cache some settings
|
||||
self.__LDAP_FILTER = ldap_setting("FILTER", "")
|
||||
self.__LDAP_SEARCH_BASE = ldap_setting("SEARCH_BASE", "")
|
||||
|
||||
# The time a cache entry is valid (in hours)
|
||||
try:
|
||||
self.__LDAP_CACHE_TTL = int(ldap_setting("CACHE_TTL", ""))
|
||||
except ValueError:
|
||||
logging.error("Invalid value for cache_ttl. Defaulting to 1 hour")
|
||||
self.__LDAP_CACHE_TTL = 1
|
||||
|
||||
password = ldap_setting("BIND_PW", "")
|
||||
if not password:
|
||||
pw_file = ldap_setting("BIND_PW_FILE", "")
|
||||
if pw_file:
|
||||
with open(pw_file, "r") as f:
|
||||
password = f.read().replace("\n", "")
|
||||
|
||||
self.__ldap_connection = ldap.initialize(ldap_setting("SERVER", ""))
|
||||
try:
|
||||
self.__ldap_connection.simple_bind_s(ldap_setting("BIND_DN", ""), password)
|
||||
except ldap.LDAPError as err:
|
||||
logging.error(f"LDAP Error occuring during bind: {err.desc}")
|
||||
|
||||
def __is_cache_valid(self, username):
|
||||
"""Returns True if the cache entry is still valid. Returns False otherwise."""
|
||||
if username in self.__user_cache:
|
||||
if timezone.now() <= self.__user_cache[username]:
|
||||
# Cache entry is still valid
|
||||
return True
|
||||
return False
|
||||
|
||||
def __remove_cache(self, username):
|
||||
del self.__user_cache[username]
|
||||
|
||||
def has_user(self, username):
|
||||
"""
|
||||
Since we don't care about the password and so authentication
|
||||
another way, all we care about is whether the user exists.
|
||||
"""
|
||||
if self.__is_cache_valid(username):
|
||||
return True
|
||||
if username in self.__user_cache:
|
||||
self.__remove_cache(username)
|
||||
|
||||
filterstr = self.__LDAP_FILTER.replace("%s", username)
|
||||
try:
|
||||
result = self.__ldap_connection.search_s(self.__LDAP_SEARCH_BASE, ldap.SCOPE_SUBTREE, filterstr=filterstr)
|
||||
except ldap.NO_RESULTS_RETURNED:
|
||||
# We handle the specific error first and the the generic error, as
|
||||
# we may expect ldap.NO_RESULTS_RETURNED, but not any other error
|
||||
return False
|
||||
except ldap.LDAPError as err:
|
||||
logging.error(f"Error occured while performing an LDAP query: {err.desc}")
|
||||
return False
|
||||
|
||||
if len(result) == 1:
|
||||
self.__user_cache[username] = timezone.now() + timezone.timedelta(hours=self.__LDAP_CACHE_TTL)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_user_in_ldap(user: UserType = Depends(get_authenticated_user)):
|
||||
if not LDAPConnection.get_instance().has_user(user.username):
|
||||
raise FastAPIPermissionDenied(detail="User not in LDAP directory.")
|
||||
|
||||
|
||||
def create_user(context: CallbackContext, *args, **kwargs):
|
||||
"""
|
||||
A create_user function which first checks if the user already exists in the
|
||||
configured LDAP directory.
|
||||
"""
|
||||
if not LDAPConnection.get_instance().has_user(kwargs["username"]):
|
||||
raise DjangoPermissionDenied("User not in the LDAP directory.")
|
||||
return User.objects.create_user(*args, **kwargs)
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 3.0.3 on 2020-05-15 08:01
|
||||
|
||||
from django.db import migrations, models
|
||||
import myauth.models
|
||||
import etebase_server.myauth.models as myauth_models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -19,7 +19,7 @@ class Migration(migrations.Migration):
|
|||
help_text="Required. 150 characters or fewer. Letters, digits and ./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[myauth.models.UnicodeUsernameValidator()],
|
||||
validators=[myauth_models.UnicodeUsernameValidator()],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 3.1.1 on 2020-11-19 08:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import myauth.models
|
||||
import etebase_server.myauth.models as myauth_models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterModelManagers(
|
||||
name="user",
|
||||
managers=[
|
||||
("objects", myauth.models.UserManager()),
|
||||
("objects", myauth_models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
|
@ -30,7 +30,7 @@ class Migration(migrations.Migration):
|
|||
help_text="Required. 150 characters or fewer. Letters, digits and ./-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[myauth.models.UnicodeUsernameValidator()],
|
||||
validators=[myauth_models.UnicodeUsernameValidator()],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
|
@ -15,7 +15,8 @@ import configparser
|
|||
from .utils import get_secret_from_file
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SOURCE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
BASE_DIR = os.path.dirname(SOURCE_DIR)
|
||||
|
||||
AUTH_USER_MODEL = "myauth.User"
|
||||
|
||||
|
@ -54,9 +55,9 @@ INSTALLED_APPS = [
|
|||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"myauth.apps.MyauthConfig",
|
||||
"django_etebase.apps.DjangoEtebaseConfig",
|
||||
"django_etebase.token_auth.apps.TokenAuthConfig",
|
||||
"etebase_server.myauth.apps.MyauthConfig",
|
||||
"etebase_server.django.apps.DjangoEtebaseConfig",
|
||||
"etebase_server.django.token_auth.apps.TokenAuthConfig",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -74,7 +75,7 @@ ROOT_URLCONF = "etebase_server.urls"
|
|||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [os.path.join(BASE_DIR, "templates")],
|
||||
"DIRS": [os.path.join(SOURCE_DIR, "templates")],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
|
@ -139,6 +140,8 @@ config_locations = [
|
|||
"/etc/etebase-server/etebase-server.ini",
|
||||
]
|
||||
|
||||
ETEBASE_CREATE_USER_FUNC = "etebase_server.django.utils.create_user_blocked"
|
||||
|
||||
# Use config file if present
|
||||
if any(os.path.isfile(x) for x in config_locations):
|
||||
config = configparser.ConfigParser()
|
||||
|
@ -164,10 +167,30 @@ if any(os.path.isfile(x) for x in config_locations):
|
|||
if "database" in config:
|
||||
DATABASES = {"default": {x.upper(): y for x, y in config.items("database")}}
|
||||
|
||||
ETEBASE_CREATE_USER_FUNC = "django_etebase.utils.create_user_blocked"
|
||||
if "database-options" in config:
|
||||
DATABASES["default"]["OPTIONS"] = config["database-options"]
|
||||
|
||||
if "ldap" in config:
|
||||
ldap = config["ldap"]
|
||||
LDAP_SERVER = ldap.get("server", "")
|
||||
LDAP_SEARCH_BASE = ldap.get("search_base", "")
|
||||
LDAP_FILTER = ldap.get("filter", "")
|
||||
LDAP_BIND_DN = ldap.get("bind_dn", "")
|
||||
LDAP_BIND_PW = ldap.get("bind_pw", "")
|
||||
LDAP_BIND_PW_FILE = ldap.get("bind_pw_file", "")
|
||||
LDAP_CACHE_TTL = ldap.get("cache_ttl", "")
|
||||
|
||||
if not LDAP_BIND_DN:
|
||||
raise Exception("LDAP enabled but bind_dn is not set!")
|
||||
if not LDAP_BIND_PW and not LDAP_BIND_PW_FILE:
|
||||
raise Exception("LDAP enabled but both bind_pw and bind_pw_file are not set!")
|
||||
|
||||
# Configure EteBase to use LDAP
|
||||
ETEBASE_CREATE_USER_FUNC = "etebase_server.myauth.ldap.create_user"
|
||||
ETEBASE_API_PERMISSIONS_READ = ["etebase_server.myauth.ldap.is_user_in_ldap"]
|
||||
|
||||
# Efficient file streaming (for large files)
|
||||
SENDFILE_BACKEND = "etebase_fastapi.sendfile.backends.simple"
|
||||
SENDFILE_BACKEND = "etebase_server.fastapi.sendfile.backends.simple"
|
||||
SENDFILE_ROOT = MEDIA_ROOT
|
||||
|
||||
# Make an `etebase_server_settings` module available to override settings.
|
||||
|
|
|
@ -1,25 +1,13 @@
|
|||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.urls import path, re_path
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.static import serve
|
||||
from django.contrib.staticfiles import finders
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^admin/", admin.site.urls),
|
||||
path("admin/", admin.site.urls),
|
||||
path("", TemplateView.as_view(template_name="success.html")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
def serve_static(request, path):
|
||||
filename = finders.find(path)
|
||||
dirname = os.path.dirname(filename)
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
return serve(request, basename, dirname)
|
||||
|
||||
urlpatterns += [re_path(r"^static/(?P<path>.*)$", serve_static)]
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.core.management import utils
|
||||
import os
|
||||
import stat
|
||||
|
||||
|
||||
def get_secret_from_file(path):
|
||||
|
@ -21,6 +23,7 @@ def get_secret_from_file(path):
|
|||
return f.read().strip()
|
||||
except EnvironmentError:
|
||||
with open(path, "w") as f:
|
||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
secret_key = utils.get_random_secret_key()
|
||||
f.write(secret_key)
|
||||
return secret_key
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# Running `etebase` under `nginx` and `uwsgi`
|
||||
|
||||
This configuration assumes that etebase server has been installed in the home folder of a non privileged user
|
||||
called `EtebaseUser` following the instructions in <https://github.com/etesync/server>. Also that static
|
||||
files have been collected at `/srv/http/etebase_server` by running the following commands:
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /srv/http/etebase_server/static
|
||||
sudo chown -R EtebaseUser /srv/http/etebase_server
|
||||
sudo su EtebaseUser
|
||||
cd /path/to/etebase
|
||||
ln -s /srv/http/etebase_server/static static
|
||||
./manage.py collectstatic
|
||||
```
|
||||
|
||||
It is also assumed that `nginx` and `uwsgi` have been installed system wide by `root`, and that `nginx` is running as user/group `www-data`.
|
||||
|
||||
In this setup, `uwsgi` running as a `systemd` service as `root` creates a unix socket with read-write access
|
||||
to both `EtebaseUser` and `nginx`. It then drops its `root` privilege and runs `etebase` as `EtebaseUser`.
|
||||
|
||||
`nginx` listens on the `https` port (or a non standard port `https` port if desired), delivers static pages directly
|
||||
and for everything else, communicates with `etebase` over the unix socket.
|
|
@ -1,15 +0,0 @@
|
|||
# uwsgi configuration file
|
||||
# typical location of this file would be /etc/uwsgi/sites/etebase.ini
|
||||
|
||||
[uwsgi]
|
||||
socket = /path/to/etebase_server.sock
|
||||
chown-socket = EtebaseUser:www-data
|
||||
chmod-socket = 660
|
||||
vacuum = true
|
||||
|
||||
|
||||
uid = EtebaseUser
|
||||
chdir = /path/to/etebase
|
||||
home = %(chdir)/.venv
|
||||
module = etebase_server.wsgi
|
||||
master = true
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue