diff --git a/README.md b/README.md index 7a7efac..d2d1361 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Create yourself an admin user: ``` At this stage you need to create accounts to be used with the EteSync apps. To do that, please go to: -`www.your-etesync-install.com/admin` and create a new user to be used with the service. No need to set +`www.your-etesync-install.com/admin` or use CLI `./manage.py createuser ` and create a new user to be used with the service. No need to set a password, as Etebase uses a zero-knowledge proof for authentication, so the user will just create a password when creating the account from the apps. @@ -152,7 +152,7 @@ A quick summary can be found [on tldrlegal](https://tldrlegal.com/license/gnu-af ## Commercial licensing -For commercial licensing options, contact license@etebase.com +For commercial licensing options, contact license@etebase.com # Financially Supporting Etebase diff --git a/django_etebase/admin-cli/management/__init__.py b/django_etebase/admin-cli/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_etebase/admin-cli/management/commands/__init__.py b/django_etebase/admin-cli/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_etebase/admin-cli/management/commands/_utils.py b/django_etebase/admin-cli/management/commands/_utils.py new file mode 100644 index 0000000..2371c78 --- /dev/null +++ b/django_etebase/admin-cli/management/commands/_utils.py @@ -0,0 +1,14 @@ +from distutils.util import strtobool +from datetime import datetime +import pytz + +def argbool(arg): + if arg == None: return None + return bool(strtobool(arg)) + +def argdate(arg): + if arg == None: return None + try: + return pytz.utc.localize(datetime.strptime(arg, '%Y-%m-%d %H:%M:%S')) + except ValueError: + return pytz.utc.localize(datetime.strptime(arg, '%Y-%m-%d %H:%M:%S.%f')) diff --git a/django_etebase/admin-cli/management/commands/group-create.py b/django_etebase/admin-cli/management/commands/group-create.py new file mode 100755 index 0000000..3f87570 --- /dev/null +++ b/django_etebase/admin-cli/management/commands/group-create.py @@ -0,0 +1,30 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import Group, Permission +from django.db.utils import IntegrityError + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument( 'name' + , type=str + , help="New group's name." ) + parser.add_argument( '--permissions' + , type=str + , nargs='*' + , default=[] + , help="New group's permissions." ) + + def handle(self, *args, **options): + try: + for index,permission in enumerate(options["permissions"]): + options["permissions"][index] = Permission.objects.get(name=permission) + + group = Group.objects.create(name=options["name"]) + group.permissions.set(options["permissions"]) + group.save() + except (IntegrityError,Permission.DoesNotExist) as exception: + self.stdout.write(self.style.ERROR(f'Unable to create group "{options["name"]}": ' + str(exception))) + exit(1) + + self.stdout.write(self.style.SUCCESS(f'Group "{options["name"]}" has been created.')) + exit(0) diff --git a/django_etebase/admin-cli/management/commands/group-delete.py b/django_etebase/admin-cli/management/commands/group-delete.py new file mode 100755 index 0000000..5fd4195 --- /dev/null +++ b/django_etebase/admin-cli/management/commands/group-delete.py @@ -0,0 +1,16 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import Group + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument( 'name' + , type=str + , help="Name of the group to be deleted." ) + + def handle(self, *args, **options): + try: + Group.objects.get(name = options["name"]).delete() + self.stdout.write(self.style.SUCCESS(f'Grop "{options["name"]}" has been deleted.')) + except Group.DoesNotExist as exception: + self.stdout.write(self.style.ERROR(f'Unable to delete group "{options["name"]}": ' + str(exception))) diff --git a/django_etebase/admin-cli/management/commands/group-modify.py b/django_etebase/admin-cli/management/commands/group-modify.py new file mode 100755 index 0000000..0ac9347 --- /dev/null +++ b/django_etebase/admin-cli/management/commands/group-modify.py @@ -0,0 +1,48 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import Group, Permission +from django.db.utils import IntegrityError + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument( 'name' + , type=str + , help="Group's name." ) + parser.add_argument( '-n' + , '--new_name' + , '--new-name' + , type=str + , default=None + , help="Group's new name." ) + parser.add_argument( '-m' + , '--mode' + , type=str + , choices=['set', 'add', 'remove'] + , default='set' + , help="Set modification mode. Affects --permissions." ) + parser.add_argument( '--permissions' + , type=str + , nargs='*' + , default=None + , help="Group's new permissions." ) + + def handle(self, *args, **options): + try: + if options["permissions"] != None: + for index,permission in enumerate(options["permissions"]): + options["permissions"][index] = Permission.objects.get(name=permission) + + group = Group.objects.get(name=options["name"]) + + if options["new_name"] != None: + group.name = options["new_name"] + if options["permissions"] != None: + if options["mode"] == "set" : group.permissions.set ( options["permissions"]) + if options["mode"] == "add" : group.permissions.add (*options["permissions"]) + if options["mode"] == "remove" : group.permissions.remove(*options["permissions"]) + + group.save() + self.stdout.write(self.style.SUCCESS(f'Group "{options["name"]}" has been modified.')) + + except (User.DoesNotExist, ValueError) as exception: + self.stdout.write(self.style.ERROR(f'Unable to modify group "{options["name"]}": ' + str(exception))) diff --git a/django_etebase/admin-cli/management/commands/groups-delete.py b/django_etebase/admin-cli/management/commands/groups-delete.py new file mode 100755 index 0000000..c89e61f --- /dev/null +++ b/django_etebase/admin-cli/management/commands/groups-delete.py @@ -0,0 +1,27 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import Group + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument( '-y' + , '--yes' + , action='store_true' + , default=False + , help="Allow deletion of all groups!" ) + + def handle(self, *args, **options): + if options["yes"] != True: + print('Do you really want to delete all groups? [y/N]: ', end='') + if input() not in ('y', 'Y', 'yes', 'YES', 'Yes'): + self.stdout.write(self.style.SUCCESS(f'No groups have been deleted.')) + exit(0) + + try: + for group in Group.objects.all(): + group.delete() + self.stdout.write(self.style.SUCCESS(f'All groups have been deleted.')) + exit(0) + except Group.DoesNotExist as exception: + self.stdout.write(self.style.ERROR(f'Unable to delete all groups: ' + str(exception))) + exit(1) diff --git a/django_etebase/admin-cli/management/commands/groups-list.py b/django_etebase/admin-cli/management/commands/groups-list.py new file mode 100755 index 0000000..181c3f0 --- /dev/null +++ b/django_etebase/admin-cli/management/commands/groups-list.py @@ -0,0 +1,8 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import Group + +class Command(BaseCommand): + + def handle(self, *args, **options): + for group in Group.objects.all(): + print(group.name) diff --git a/django_etebase/admin-cli/management/commands/permissions-list.py b/django_etebase/admin-cli/management/commands/permissions-list.py new file mode 100755 index 0000000..9697e5d --- /dev/null +++ b/django_etebase/admin-cli/management/commands/permissions-list.py @@ -0,0 +1,8 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import Permission + +class Command(BaseCommand): + + def handle(self, *args, **options): + for permission in Permission.objects.all(): + print(permission.name) diff --git a/django_etebase/admin-cli/management/commands/user-create.py b/django_etebase/admin-cli/management/commands/user-create.py new file mode 100755 index 0000000..8e0a730 --- /dev/null +++ b/django_etebase/admin-cli/management/commands/user-create.py @@ -0,0 +1,111 @@ +from django.core.management.base import BaseCommand +from django_etebase.users.management.commands._utils import argbool, argdate +from myauth.models import User +from django.contrib.auth.models import Group, Permission +from django.db.utils import IntegrityError + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument( 'username' + , type=str + , help="New user's login username." ) + parser.add_argument( '-p' + , '--password' + , type=str + , help="New user's plain text login password." ) + parser.add_argument( '-f' + , '--first_name' + , '--first' + , type=str + , default='' + , help="New user's first name." ) + parser.add_argument( '-l' + , '--last_name' + , '--last' + , type=str + , default='' + , help="New user's last name." ) + parser.add_argument( '-e' + , '--email' + , type=str + , default='' + , help="New user's email address." ) + parser.add_argument( '-a' + , '--is_active' + , '--active' + , nargs='?' + , type=argbool + , const=True + , default=False + , help="Enable login. [YES]" ) + parser.add_argument( '-s' + , '--is_staff' + , '--staff' + , nargs='?' + , type=argbool + , const=True + , default=False + , help="Mark user as staff. [NO]" ) + parser.add_argument( '-S' + , '--is_superuser' + , '--superuser' + , nargs='?' + , type=argbool + , const=True + , default=False + , help="Mark user as superuser. [NO]" ) + parser.add_argument( '-g' + , '--groups' + , type=str + , nargs='*' + , default=[] + , help="New user's groups." ) + parser.add_argument( '--user_permissions' + , '--user-permissions' + , '--permissions' + , type=str + , nargs='*' + , default=[] + , help="New user's user permissions." ) + parser.add_argument( '-j' + , '--date_joined' + , '--date-joined' + , type=str + , default=None + , help="New user's date joined, formated as '%Y-%m-%d %H:%M:%S.%f'." ) + parser.add_argument( '--last_login' + , '--last-login' + , type=str + , default=None + , help="New user's last login date, formated as '%Y-%m-%d %H:%M:%S.%f'." ) + + def handle(self, *args, **options): + try: + for index,group in enumerate(options["groups"]): + options["groups"][index] = Group.objects.get(name=group) + for index,permission in enumerate(options["user_permissions"]): + options["user_permissions"][index] = Permission.objects.get(name=permission) + options["date_joined"] = argdate(options["date_joined"]) + options["last_login" ] = argdate(options["last_login" ]) + + user = User.objects.create_user( username = options["username" ] + , password = options["password" ] + , email = options["email" ] + , first_name = options["first_name" ] + , last_name = options["last_name" ] + , is_superuser = options["is_superuser" ] + , is_staff = options["is_staff" ] + , is_active = options["is_active" ] + , last_login = options["last_login" ] ) + user.groups.set(options["groups"]) + user.user_permissions.set(options["user_permissions"]) + if options["date_joined"] != None: + user.date_joined = options["date_joined"] + user.save() + except (IntegrityError,Group.DoesNotExist,Permission.DoesNotExist) as exception: + self.stdout.write(self.style.ERROR(f'Unable to create user "{options["username"]}": ' + str(exception))) + exit(1) + + self.stdout.write(self.style.SUCCESS(f'User "{options["username"]}" has been created.')) + exit(0) diff --git a/django_etebase/admin-cli/management/commands/user-delete.py b/django_etebase/admin-cli/management/commands/user-delete.py new file mode 100755 index 0000000..9a35d8d --- /dev/null +++ b/django_etebase/admin-cli/management/commands/user-delete.py @@ -0,0 +1,16 @@ +from django.core.management.base import BaseCommand +from myauth.models import User + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument( 'username' + , type=str + , help="Login username of the user to be deleted." ) + + def handle(self, *args, **options): + try: + User.objects.get(username = options["username"]).delete() + self.stdout.write(self.style.SUCCESS(f'User "{options["username"]}" has been deleted.')) + except User.DoesNotExist as exception: + self.stdout.write(self.style.ERROR(f'Unable to delete user "{options["username"]}": ' + str(exception))) diff --git a/django_etebase/admin-cli/management/commands/user-modify.py b/django_etebase/admin-cli/management/commands/user-modify.py new file mode 100755 index 0000000..bf39e68 --- /dev/null +++ b/django_etebase/admin-cli/management/commands/user-modify.py @@ -0,0 +1,137 @@ +from django.core.management.base import BaseCommand +from django_etebase.users.management.commands._utils import argbool, argdate +from myauth.models import User +from django.contrib.auth.models import Group, Permission +from django.db.utils import IntegrityError + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument( 'username' + , type=str + , help="User's login username." ) + parser.add_argument( '-u' + , '--new_username' + , '--new-username' + , type=str + , default=None + , help="User's new login username." ) + parser.add_argument( '-p' + , '--password' + , type=str + , help="User's new plain text login password." ) + parser.add_argument( '-f' + , '--first_name' + , '--first-name' + , '--first' + , type=str + , default=None + , help="User's new first name." ) + parser.add_argument( '-l' + , '--last_name' + , '--last-name' + , '--last' + , type=str + , default=None + , help="User's new last name." ) + parser.add_argument( '-e' + , '--email' + , type=str + , default=None + , help="User's new email address." ) + parser.add_argument( '-a' + , '--is_active' + , '--is-active' + , '--active' + , nargs='?' + , type=argbool + , const=True + , default=None + , help="Enable/Disable login." ) + parser.add_argument( '-s' + , '--is_staff' + , '--is-staff' + , '--staff' + , nargs='?' + , type=argbool + , const=True + , default=None + , help="Mark/Unmark user as staff." ) + parser.add_argument( '-S' + , '--is_superuser' + , '--is-superuser' + , '--superuser' + , nargs='?' + , type=argbool + , const=True + , default=None + , help="Mark/Unmark user as superuser." ) + parser.add_argument( '-m' + , '--mode' + , type=str + , choices=['set', 'add', 'remove'] + , default='set' + , help="Set modification mode. Affects --groups and --user_permissions." ) + parser.add_argument( '-g' + , '--groups' + , type=str + , nargs='*' + , default=None + , help="User's new groups." ) + parser.add_argument( '--user_permissions' + , '--user-permissions' + , '--permissions' + , type=str + , nargs='*' + , default=None + , help="User's new user permissions." ) + parser.add_argument( '-j' + , '--date_joined' + , '--date-joined' + , type=str + , default=None + , help="User's new date joined, formated as '%Y-%m-%d %H:%M:%S.%f'." ) + parser.add_argument( '--last_login' + , '--last-login' + , type=str + , default=None + , help="User's new last login date, formated as '%Y-%m-%d %H:%M:%S.%f'." ) + + def handle(self, *args, **options): + try: + if options["groups"] != None: + for index,group in enumerate(options["groups"]): + options["groups"][index] = Group.objects.get(name=group) + if options["user_permissions"] != None: + for index,permission in enumerate(options["user_permissions"]): + options["user_permissions"][index] = Permission.objects.get(name=permission) + options["date_joined"] = argdate(options["date_joined"]) + options["last_login" ] = argdate(options["last_login" ]) + + user = User.objects.get(username = options["username"]) + + if options["new_username"] != None: user.username = options["new_username"] + if options["password" ] != None: user.password = options["password" ] + if options["email" ] != None: user.email = options["email" ] + if options["first_name" ] != None: user.first_name = options["first_name" ] + if options["last_name" ] != None: user.last_name = options["last_name" ] + if options["is_active" ] != None: user.is_active = options["is_active" ] + if options["is_staff" ] != None: user.is_staff = options["is_staff" ] + if options["is_superuser"] != None: user.is_superuser = options["is_superuser"] + if options["date_joined" ] != None: user.date_joined = options["date_joined" ] + if options["last_login" ] != None: user.last_login = options["last_login" ] + + if options["groups"] != None: + if options["mode"] == "set" : user.groups.set ( options["groups"]) + if options["mode"] == "add" : user.groups.add (*options["groups"]) + if options["mode"] == "remove" : user.groups.remove(*options["groups"]) + if options["user_permissions"] != None: + if options["mode"] == "set" : user.user_permissions.set ( options["user_permissions"]) + if options["mode"] == "add" : user.user_permissions.add (*options["user_permissions"]) + if options["mode"] == "remove" : user.user_permissions.remove(*options["user_permissions"]) + + user.save() + self.stdout.write(self.style.SUCCESS(f'User "{options["username"]}" has been modified.')) + + except (User.DoesNotExist, ValueError) as exception: + self.stdout.write(self.style.ERROR(f'Unable to modify user "{options["username"]}": ' + str(exception))) diff --git a/django_etebase/admin-cli/management/commands/users-delete.py b/django_etebase/admin-cli/management/commands/users-delete.py new file mode 100755 index 0000000..619e245 --- /dev/null +++ b/django_etebase/admin-cli/management/commands/users-delete.py @@ -0,0 +1,25 @@ +from django.core.management.base import BaseCommand +from myauth.models import User + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument( '-y' + , '--yes' + , action='store_true' + , default=False + , help="Allow deletion of all users!" ) + + def handle(self, *args, **options): + if options["yes"] != True: + print('Do you really want to delete all users? [y/N]: ', end='') + if input()[0] not in ('y', 'Y', 'yes', 'YES', 'Yes'): + self.stdout.write(self.style.SUCCESS(f'No users have been deleted.')) + exit(0) + + try: + for user in User.objects.all(): + user.delete() + self.stdout.write(self.style.SUCCESS(f'All users have been deleted.')) + except User.DoesNotExist as exception: + self.stdout.write(self.style.ERROR(f'Unable to delete all users: ' + str(exception))) diff --git a/django_etebase/admin-cli/management/commands/users-list.py b/django_etebase/admin-cli/management/commands/users-list.py new file mode 100755 index 0000000..860385e --- /dev/null +++ b/django_etebase/admin-cli/management/commands/users-list.py @@ -0,0 +1,8 @@ +from django.core.management.base import BaseCommand +from myauth.models import User + +class Command(BaseCommand): + + def handle(self, *args, **options): + for user in User.objects.all(): + print(user.username) diff --git a/etebase_server/settings.py b/etebase_server/settings.py index aa77461..8f7e0ea 100644 --- a/etebase_server/settings.py +++ b/etebase_server/settings.py @@ -56,6 +56,7 @@ INSTALLED_APPS = [ "myauth.apps.MyauthConfig", "django_etebase.apps.DjangoEtebaseConfig", "django_etebase.token_auth.apps.TokenAuthConfig", + "django_etebase.admin-cli", ] MIDDLEWARE = [