-
-
Notifications
You must be signed in to change notification settings - Fork 227
[feature] Made RegisteredUser model support multi-tenancy #692 #698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
46f8a58
7010cfc
d83228d
c88a132
1dd7a39
24d39f9
55580f9
bbcdd72
4cc1dc6
7e76f86
cbef5fa
6e2fb88
1dc6655
e1ff16e
a418450
6fcec64
7761257
d52deff
104a7cd
31132cc
b83ca76
554eccf
b61ea07
c1de2d7
08a7bdf
bfcf393
f5c6d92
b3f99ba
93d39da
494c9fc
1322a9f
31a6740
61e05ba
2b741c4
f63a522
8dc28c3
870ad8f
aad1704
9d382b4
7d9601d
46e5267
06687cf
938b852
fd60fba
cff9fa4
fea912f
ec73d05
a031f38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -36,7 +36,6 @@ | |||||||||||||||||
| from .. import settings as app_settings | ||||||||||||||||||
| from ..base.forms import PasswordResetForm | ||||||||||||||||||
| from ..counters.exceptions import SkipCheck | ||||||||||||||||||
| from ..registration import REGISTRATION_METHOD_CHOICES | ||||||||||||||||||
| from ..utils import ( | ||||||||||||||||||
| get_group_checks, | ||||||||||||||||||
| get_organization_radius_settings, | ||||||||||||||||||
|
|
@@ -571,9 +570,13 @@ class RegisterSerializer( | |||||||||||||||||
| 'verification in its "Organization RADIUS Settings."' | ||||||||||||||||||
| ), | ||||||||||||||||||
| default="", | ||||||||||||||||||
| choices=REGISTRATION_METHOD_CHOICES, | ||||||||||||||||||
| choices=(), | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| def __init__(self, *args, **kwargs): | ||||||||||||||||||
| super().__init__(*args, **kwargs) | ||||||||||||||||||
| self.fields["method"].choices = app_settings.USER_SETTABLE_REGISTRATION_METHODS | ||||||||||||||||||
|
|
||||||||||||||||||
| def validate_phone_number(self, phone_number): | ||||||||||||||||||
| org = self.context["view"].organization | ||||||||||||||||||
| if get_organization_radius_settings(org, "sms_verification"): | ||||||||||||||||||
|
|
@@ -688,9 +691,11 @@ def save(self, request): | |||||||||||||||||
| # the custom_signup method contains the openwisp specific logic | ||||||||||||||||||
| self.custom_signup(request, user) | ||||||||||||||||||
| # create a RegisteredUser object for every user that registers through API | ||||||||||||||||||
| RegisteredUser.objects.create( | ||||||||||||||||||
| org = self.context["view"].organization | ||||||||||||||||||
| RegisteredUser.get_or_create_for_user_and_org( | ||||||||||||||||||
| user=user, | ||||||||||||||||||
| method=self.validated_data["method"], | ||||||||||||||||||
| organization=org, | ||||||||||||||||||
| defaults={"method": self.validated_data["method"]}, | ||||||||||||||||||
| ) | ||||||||||||||||||
| setup_user_email(request, user, []) | ||||||||||||||||||
| return user | ||||||||||||||||||
|
|
@@ -753,20 +758,64 @@ def save(self): | |||||||||||||||||
| # yet, tha will be done by the phone token validation view | ||||||||||||||||||
| # once the phone number has been validated | ||||||||||||||||||
| # at this point we flag the user as unverified again | ||||||||||||||||||
| self.user.registered_user.is_verified = False | ||||||||||||||||||
| self.user.registered_user.save() | ||||||||||||||||||
| org = self.context["view"].organization | ||||||||||||||||||
| reg_user, _ = RegisteredUser.get_or_create_for_user_and_org( | ||||||||||||||||||
| user=self.user, | ||||||||||||||||||
| organization=org, | ||||||||||||||||||
| defaults={"is_verified": False, "method": ""}, | ||||||||||||||||||
| ) | ||||||||||||||||||
| reg_user.is_verified = False | ||||||||||||||||||
| reg_user.save() | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class UpdateRegisteredUserMethodSerializer(ValidatedModelSerializer): | ||||||||||||||||||
| method = serializers.ChoiceField( | ||||||||||||||||||
| choices=app_settings.USER_SETTABLE_REGISTRATION_METHODS, | ||||||||||||||||||
| help_text=_( | ||||||||||||||||||
| "The registration method to set for the user. " | ||||||||||||||||||
| "Cannot be 'pending_verification'." | ||||||||||||||||||
| ), | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| class Meta: | ||||||||||||||||||
| model = RegisteredUser | ||||||||||||||||||
| fields = ["method"] | ||||||||||||||||||
|
|
||||||||||||||||||
| def __init__(self, *args, **kwargs): | ||||||||||||||||||
| super().__init__(*args, **kwargs) | ||||||||||||||||||
| self.fields["method"].choices = app_settings.USER_SETTABLE_REGISTRATION_METHODS | ||||||||||||||||||
|
|
||||||||||||||||||
| def validate_method(self, value): | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think we can blindly trust all registered method values here. Some values are not normal user choices: they represent server side flows which should set the method only after that flow actually happened. Practical examples are Right now, could a user submit Please let me know what you think. If you think it cannot be abused, explain why. Otherwise let's discuss the right way to restrict this endpoint before changing it, because a quick allowlist/denylist could also be wrong if we do not define the intended flow clearly.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nemesifier the RegisterSerializer already accepts a method field openwisp-radius/openwisp_radius/api/serializers.py Lines 568 to 575 in bf61cdb
So the tightening of the allowed registration choices should also apply there.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. We can add another setting which specifies with registration methods are self initiated. |
||||||||||||||||||
| if value == "pending_verification": | ||||||||||||||||||
| raise serializers.ValidationError( | ||||||||||||||||||
| _("'pending_verification' cannot be set as a registration method.") | ||||||||||||||||||
| ) | ||||||||||||||||||
| return value | ||||||||||||||||||
|
|
||||||||||||||||||
| def validate(self, attrs): | ||||||||||||||||||
| if self.instance.method != "pending_verification": | ||||||||||||||||||
| raise serializers.ValidationError( | ||||||||||||||||||
| { | ||||||||||||||||||
| "method": _( | ||||||||||||||||||
| "Method can only be updated from pending verification state." | ||||||||||||||||||
| ) | ||||||||||||||||||
| } | ||||||||||||||||||
| ) | ||||||||||||||||||
| return attrs | ||||||||||||||||||
|
|
||||||||||||||||||
| def update(self, instance, validated_data): | ||||||||||||||||||
| instance.method = validated_data["method"] | ||||||||||||||||||
| instance.save() | ||||||||||||||||||
| return instance | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class RadiusUserSerializer(serializers.ModelSerializer): | ||||||||||||||||||
| """ | ||||||||||||||||||
| Used to return information about the logged in user | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| is_verified = serializers.BooleanField(source="registered_user.is_verified") | ||||||||||||||||||
| method = serializers.CharField( | ||||||||||||||||||
| source="registered_user.method", | ||||||||||||||||||
| allow_null=True, | ||||||||||||||||||
| ) | ||||||||||||||||||
| is_verified = serializers.SerializerMethodField() | ||||||||||||||||||
| method = serializers.SerializerMethodField() | ||||||||||||||||||
| password_expired = serializers.BooleanField(source="has_password_expired") | ||||||||||||||||||
| radius_user_token = serializers.CharField(source="radius_token.key", default=None) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -786,3 +835,30 @@ class Meta: | |||||||||||||||||
| "password_expired", | ||||||||||||||||||
| "radius_user_token", | ||||||||||||||||||
| ] | ||||||||||||||||||
|
|
||||||||||||||||||
| def _get_registered_user(self, obj): | ||||||||||||||||||
| if not hasattr(self, "_registered_user_cache"): | ||||||||||||||||||
| self._registered_user_cache = {} | ||||||||||||||||||
| if obj.pk not in self._registered_user_cache: | ||||||||||||||||||
| view = self.context.get("view") | ||||||||||||||||||
| organization = getattr(view, "organization", None) | ||||||||||||||||||
| reg_user = None | ||||||||||||||||||
| # We iterate over .all() instead of using .filter() because callers | ||||||||||||||||||
| # of this serializer (e.g. validate_auth_token) prefetch | ||||||||||||||||||
| # "registered_users" via prefetch_related. Using .all() hits the | ||||||||||||||||||
| # in-memory prefetch cache (0 DB queries), whereas .filter() would | ||||||||||||||||||
| # bypass the cache and issue a new query every time. | ||||||||||||||||||
| for ru in obj.registered_users.all(): | ||||||||||||||||||
| if organization and ru.organization_id == organization.pk: | ||||||||||||||||||
| reg_user = ru | ||||||||||||||||||
| break | ||||||||||||||||||
| self._registered_user_cache[obj.pk] = reg_user | ||||||||||||||||||
| return self._registered_user_cache[obj.pk] | ||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||
|
|
||||||||||||||||||
| def get_is_verified(self, obj): | ||||||||||||||||||
| reg_user = self._get_registered_user(obj) | ||||||||||||||||||
| return reg_user.is_verified if reg_user else None | ||||||||||||||||||
|
|
||||||||||||||||||
| def get_method(self, obj): | ||||||||||||||||||
| reg_user = self._get_registered_user(obj) | ||||||||||||||||||
| return reg_user.method if reg_user else None | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This describes the flow inaccurately. The implementation already creates the OrganizationUser and a pending RegisteredUser row during cross organization login when registration is enabled; access remains denied until org specific verification is completed. Please reword this to avoid saying the account is created only after verification.