From 9f27bc55593affef23da5dd3e67f1b91fa7ae983 Mon Sep 17 00:00:00 2001 From: ali <117142933+muhammad-ali-e@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:28:22 +0530 Subject: [PATCH] changes in user crdential updations and readme (#863) * changes in user crdential updations and readme * update man Read me by adding a linnk of Authentication of backend readme * Update README.md Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com> Signed-off-by: ali <117142933+muhammad-ali-e@users.noreply.github.com> * Update README.md Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com> Signed-off-by: ali <117142933+muhammad-ali-e@users.noreply.github.com> * Update backend/README.md Co-authored-by: Hari John Kuriakose Signed-off-by: ali <117142933+muhammad-ali-e@users.noreply.github.com> * minor type and logs fixes --------- Signed-off-by: ali <117142933+muhammad-ali-e@users.noreply.github.com> Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com> Co-authored-by: Hari John Kuriakose --- README.md | 7 +- backend/README.md | 24 +++- backend/account_v2/authentication_service.py | 122 +++++++++++++++--- .../organization_member_service.py | 24 ++++ 4 files changed, 147 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 2d3dde6bb..6d75357cb 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,15 @@ Next, either download a release or clone this repo and do the following: ✅ `./run-platform.sh`
✅ Now visit [http://frontend.unstract.localhost](http://frontend.unstract.localhost) in your browser
-✅ Use user name and password `unstract` to login +✅ Use username and password `unstract` to login + That's all there is to it! +Follow [these steps](backend/README.md#authentication) to change the default username and password. + See [user guide](https://docs.unstract.com/unstract/unstract_platform/user_guides/run_platform) for more details on managing the platform. -Another really quick way to experience Unstract is by signing up for our [hosted version](https://us-central.unstract.com/). +Another really quick way to experience Unstract is by signing up for our [hosted version](https://us-central.unstract.com/). It comes with a 14 day free trial! ## ⏩ Quick Start Guide diff --git a/backend/README.md b/backend/README.md index a048efec8..8f8cbc330 100644 --- a/backend/README.md +++ b/backend/README.md @@ -74,20 +74,30 @@ python manage.py runserver localhost:8000 The default username is `unstract` and the default password is `unstract`. +### Initial Setup + To customize the username or password: 1. Navigate to `/backend/.env` created from [/backend/sample.env](/backend/sample.env) -1. Update the values for `DEFAULT_AUTH_USERNAME` and `DEFAULT_AUTH_PASSWORD` with strong, unique credentials of your choosing -1. Save the `/backend/.env` file and restart the server to apply changes +2. Update the values for `DEFAULT_AUTH_USERNAME` and `DEFAULT_AUTH_PASSWORD` with strong, unique credentials of your choosing +3. Save the `/backend/.env` file and restart the server to apply changes + +### Updating Credentials + +To update the username or password after initial setup: -> **NOTE**: The username `admin` is reserved for Django admin, hence cannot be used +1. Modify the credentials in `/backend/.env` + - **DEFAULT_AUTH_USERNAME**=`your_new_username` + - **DEFAULT_AUTH_PASSWORD**=`your_new_password` +2. Save changes and restart `backend` service -To update the username or password after it's been set: +Now you can login with the new credentials. -1. Modify the username and password in the same `/backend/.env` -1. Restart server to apply updates -1. Login with the new credentials +### Important Notes +- **DEFAULT_AUTH_USERNAME** must not match the `username` of any `Django superuser` or `admin` account. Keeping them distinct ensures security and avoids potential conflicts. +- Use strong and unique credentials to protect your system. +- The authentication system validates credentials against the values specified in the `/backend/.env` file. ## Asynchronous Execution diff --git a/backend/account_v2/authentication_service.py b/backend/account_v2/authentication_service.py index 59be49c44..7f2f746cf 100644 --- a/backend/account_v2/authentication_service.py +++ b/backend/account_v2/authentication_service.py @@ -27,8 +27,9 @@ from rest_framework.response import Response from tenant_account_v2.models import OrganizationMember as OrganizationMember from tenant_account_v2.organization_member_service import OrganizationMemberService +from utils.user_context import UserContext -Logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class AuthenticationService: @@ -90,6 +91,13 @@ def authenticate_and_login( bool: True if the user is successfully authenticated and logged in, False otherwise. """ + # Validation of user credentials + if ( + username != DefaultOrg.MOCK_USER + or password != DefaultOrg.MOCK_USER_PASSWORD + ): + return False + user = authenticate(request, username=username, password=password) if user: # To avoid conflicts with django superuser @@ -98,7 +106,7 @@ def authenticate_and_login( login(request, user) return True # Attempt to initiate default user and authenticate again - if self.set_default_user(username, password): + if self._set_default_user(): user = authenticate(request, username=username, password=password) if user: login(request, user) @@ -295,36 +303,108 @@ def get_organization_by_org_id(self, id: str) -> OrganizationData: ) return organizationData - def set_default_user(self, username: str, password: str) -> bool: + def _set_default_user(self) -> bool: """Set the default user for authentication. - This method creates a default user with the provided username and - password if the username and password match the default values defined - in the 'DefaultOrg' class. The default user is saved in the database. + Creates or updates a default user with predefined credentials. + + Returns: + bool: True if the default user is successfully created/updated. + """ + try: + UserContext.set_organization_identifier(DefaultOrg.ORGANIZATION_NAME) + organization = UserContext.get_organization() + + user = self._get_or_create_user(organization) + self._update_user_credentials(user) + + return True + + except Exception as e: + logger.error(f"Failed to set default user: {str(e)}") + return False + + def _get_or_create_user(self, organization: Optional[Organization]) -> User: + """Get existing user or create a new one based on organization context. Args: - username (str): The username of the default user. - password (str): The password of the default user. + organization: The organization context Returns: - bool: True if the default user is successfully created and saved, - False otherwise. + User: The retrieved or created user """ - if ( - username != DefaultOrg.MOCK_USER - or password != DefaultOrg.MOCK_USER_PASSWORD - ): - return False + if not organization: + return self._create_mock_user() + return self._get_or_create_organization_user() + + def _create_mock_user(self) -> User: + """Create a new mock user if it doesn't exist. + + Returns: + User: The created or existing mock user + """ user, created = User.objects.get_or_create(username=DefaultOrg.MOCK_USER) if created: - user.password = make_password(DefaultOrg.MOCK_USER_PASSWORD) - else: - user.user_id = DefaultOrg.MOCK_USER_ID - user.email = DefaultOrg.MOCK_USER_EMAIL - user.password = make_password(DefaultOrg.MOCK_USER_PASSWORD) + logger.info(f"Created new user with username {DefaultOrg.MOCK_USER}") + return user + + def _get_or_create_organization_user(self) -> User: + """Get or create an organization user with admin privileges. + + Returns: + User: The organization user with admin role + """ + admin_user = self._get_admin_user() + if admin_user: + return admin_user + + member = self._promote_first_member_to_admin() + if member: + return member.user + + return self._create_mock_user() + + def _get_admin_user(self) -> Optional[User]: + """Get the first admin user from the organization. + + Returns: + Optional[User]: The admin user if exists, None otherwise + """ + admin_members = OrganizationMemberService.get_members_by_role( + role=UserRole.ADMIN.value + ) + return admin_members[0].user if admin_members else None + + def _promote_first_member_to_admin(self) -> Optional[OrganizationMember]: + """Promote the first organization member to admin role. + + Returns: + Optional[OrganizationMember]: The promoted member if exists, None otherwise + """ + members = OrganizationMemberService.get_members() + if not members: + logger.warning("No organization member found") + return None + + first_member = members[0] + OrganizationMemberService.set_member_role( + member_id=first_member.member_id, role=UserRole.ADMIN.value + ) + return first_member + + def _update_user_credentials(self, user: User) -> None: + """Update user with default credentials. + + Args: + user (User): The user to update + """ + user.username = DefaultOrg.MOCK_USER + user.user_id = DefaultOrg.MOCK_USER_ID + user.email = DefaultOrg.MOCK_USER_EMAIL + user.password = make_password(DefaultOrg.MOCK_USER_PASSWORD) user.save() - return True + logger.info(f"Updated user {user} with username {DefaultOrg.MOCK_USER}") def get_user_info(self, request: Request) -> Optional[UserInfo]: user: User = request.user diff --git a/backend/tenant_account_v2/organization_member_service.py b/backend/tenant_account_v2/organization_member_service.py index ae9dd085c..71fa4f215 100644 --- a/backend/tenant_account_v2/organization_member_service.py +++ b/backend/tenant_account_v2/organization_member_service.py @@ -31,6 +31,30 @@ def get_user_by_id(id: str) -> Optional[OrganizationMember]: def get_members() -> list[OrganizationMember]: return OrganizationMember.objects.all() + @staticmethod + def get_members_by_role(role: str) -> list[OrganizationMember]: + """It return members in the order of member_id + + Args: + role (str): user role + + Returns: + list[OrganizationMember]: list of members + """ + return OrganizationMember.objects.filter(role=role).order_by("member_id") + + @staticmethod + def set_member_role(member_id: int, role: str) -> None: + """Set the role of a member. + + Parameters: + role (str): The role to set. + """ + # Get and update member + member = OrganizationMember.objects.get(member_id=member_id) + member.role = role.lower() + member.save() + @staticmethod def get_members_by_user_email( user_emails: list[str], values_list_fields: list[str]