DAVIDhaker/django-sso

Feature request: custom user fields

kakulukia opened this issue · 35 comments

Please let me define custom user model fields which are included when data is transferred to SSO clients.

This way i can transfer additional data like the displayed name in our forum.

Good feature. I'll think about it, how to do this.

I might be able to help out. Your planned visuals for the SSO flow might help, tho!

I think about format. Variants:

  1. Just array in settings.py
  2. Hook function in settings who will add custom fields to sending data ( string to function like: 'my_proj.sso_overrides.custom_fields'. Like this.
    Gateway side:
def sso_fields_hook(user, event_fields):
    event_fields['custom_dynamic_field'] = f'{user.login} #{user.id}' 
    return event_fields

Subordinated side:
Overriding event acceptors. Or extending it for mapping posibility.
3. Like the EventAcceptors - make interface for customization of communications.
4. Review of the event emiting mechanism and make something like EventAcceptor.

I think option number 1 sounds good enough and easy to understand.

I started to work.

Ok, i have the basic wiring done and have to add another argument to this ticket: The SSO client might have some additional and required user fields that have to be filled in order to save a new user object.

The concrete example: im trying to add Zulip (https://zulip.com/) as SSO client and thanks to your error logging i can see that at least one required field is missing: null value in column "realm_id" violates not-null constraint

So, it would be cool to not only define fields from the server side user model to copy to the client, but also to define default values or a function name accepting a user object and giving back the additional field values (in a dict?).

Please think about it.

@kakulukia. Already:

Next text from v2 documentation:

Send additional data to subordinated services

By default the SSO gateway sends to subordinated services next fields (if provided in user model):

  • is_staff - Are user is staff member (can access admin panel).

  • enabled - Obviously, activity status.

  • is_superuser - Are user have full privileges.

But possible cases when you need to send more data, than only this flags. For this case the library have next way:

Imagine, that we have next:

# Anything additional data
class Category(models.Model):
    name = models.CharField(max_length=12)
    
    def to_sso_representation(self):
        """
        This method will called if this instance will passed as one variable
        """
        return f'{self.name} :)'
    
    @property
    def my_var(self):
        retutn 123
    
    def my_method(self):
        """
        Your method that returns data (should be too fast)
        - it is recommended to mark this method for security reasons, as it is used in the SSO mechanism
        - recommended to be named like "sso_field__my_id_is" for code style and obviousness
        """
        return f'My id are {self.id}'

# Custom user model or the default auth.User model (no difference)
class UserModel(AbstractUser):
    my_custom_field = CharField(max_length=16, default=None, null=True)
    category = models.ForeignKey(Category, default=None, null=True, on_delete=models.CASCADE)

# The One2One related model which extends user model (possible that you create this instance via signals)
class AdvancedUserData(models.Model):
    user = models.OneToOneField(UserModel, on_delete=models.CASCADE, related_name='data')
    my_data_field = models.IntegerField(default=1)
  1. On the gateway side in settings.py add variable with implementation like next:
	SSO = {
	    # Additional fields wich send with update user event (Any fields related to user model)
	    # This setting are optional
		'ADDITIONAL_FIELDS': (
	    	# It's a UserModel.my_custom_field
	    	'my_custom_field', 
	    
	     	# It's a Category.name from UserModel.category
	    	'category.name',
	    
	    	# Will put Category.to_sso_representation (or __str__) by UserModel.category object
	    	'category',
	    
	    	# If prop isset - will put AdvancedUserData.my_data_field
	    	'data.my_data_field',
	    
	    	# Will get value from the AdvancedUserData.to_sso_representation method
	    	# or AdvancedUserData.__str__
	    	'data', 
	    
	    	# This field is UserModel.my_custom_field with an alias named "an_alias"
	    	'my_custom_field:an_alias',
	    
	    	# Also you can put properties and methods to sending data
	    	'category.my_method', # Category.my_method will called
	    	'category.my_var', # Get the Category.my_var prop value
		)
	}

  • Value, that you put to fields, must be bool or str or int or float. If expected any other type: the django-sso library will try to cast to str or will raise exception.
  • When you will set the SSO_ADDITIONAL_FIELDS variable - system will subscribe to all post_save signals of provided models and will get all described values and will emit event with it. Event emits when any model has changed/deleted.
  • All finaly translated (to aliases or not) field names must be distinct and not overrides the basic variables (is_staff, enabled, is_superuser) else you will catch exception on project start.
  • Any field may be aliased
  • If methods execution time greater than 200ms - you got a detailed warning in console.

Sounds good, maybe a bit too complex, but maybe even in Zulip i might need to save additional models. How do you know where to save category.name to?

With enabled i bet you mean in fact the default attribute of is_active?

It looks like "def to_sso_representation(self)" has to live in AdvancedUserData.

'# This field is UserModel.my_custom_field with an alias named "an_alias"' << not sure if i understand that correct, but if it is what i think it is, i would change the comment to:
'# The field UserModel.my_custom_field will be sent with an aliased name of "an_alias"'

Thanks for word fixings. On client side variables also will be mapped. Or by EventAcceptor class or by other settings. Work in progress - i'm in thinks.

Work in progress. I'll backup code to draft/v2 branch...

I saw some update commits in the new branch .. whats the current status?

Last changes i doing now and in 48h will push v2 to master and pypi.

Wow, thanks and looking forward to it!

But found the problem until I know how to solve it perfectly. First, I made it possible to add related model fields. But in the case when many users are related to one object and this object will change, the SSO library can arrange a storm of events. So far I have only one solution:

Imagine you have a user model and a category associated with it. And to configure SSO[ADDITIONAL_FIELDS] you specify, for example, category.name. And when a category is changed or deleted, the system will issue an additional event in the following format:

# For each model - a separate event with part of fields model owned
{
      "type": "update_fields",
      "fields": [
          "category.name": "New name",
      ],
      "user_identities": [
          "admin",
          "username1",
          "etc"
      ]
}

Why not just issue one event for the related category item instead?

But anyway I think this is much more than is needed for this project to be a good SSO solution. I would rather cut the related model stuff because of unforseen consequences. Or let's put it that way. Let's have syncable attributes first and related models in another version.

@kakulukia, you are right! For better solution of described problem need more time for me. And i do not wanna to hurry to produce new features. It's library, not project under deadline. Because for now i do not have end and better solution.

But could you please release the simple attribute transfer? (without complex send level relations)

Yes! I feel, that you hurry. And now i doing this. I planning today to produce v2.

Awesome!

I need an SSO solution for a project I'm working on. I was trying several different methods, which didn't work and felt clunky. Your library felt very clean and like the way to go. There is just that little bit missing. Looking forward to the new release!

I might have something for you in return. I saw that you are doing DevOps. Have a look at my dotfiles repo. You might like it.

Already familiarized with the "dotfiles" in your repository. Its wonderful and amazing. I planned to try to use it little bit later.

If not secret: how many users in you project will? (You can mail me this number)

Currently there are around 80 users that will have access to the Zulip chat tool. The number shall rise to a few hundreds within this year. Nothing a few signals can't handle as they won't ever login simultaneous.

I'm glad you already found my pimped shell. You will never want to use a default bash again!
Leave a star and some comments.
And be sure to install all the extras or at least ripgrep, fd and bat as I plan to include em as default tools in the next release. If you have some time just install all extras and try em out.

Hello, @kakulukia. Excuse me for lag. 2 last days did spend to real life. Was deals. I remember about your hury. How many days you have to the deadline for integration of this library? Tomorrow and until Monday i have free and will work for this issue.

Well, there is no real deadline, but the sooner the better.
I also have to do some more preparations.

Hey @kakulukia. Thanks for waiting. Work in progress. I found a great solution: the possibility of only one level of nesting. In other (and more rightly) words - only direct models relationships are allowed for custom fields.

This way excludes any problems i did think and stuck.

I working on, despite job place changes.

Soon as possible - i will deploy v2.

It's a few days, i think.

Could i help out in some way?

No, thanks. Hard part is done. Remained obvious changes.

Sounds good!

Hello, @kakulukia! I near end. Thanks for waiting. All is done. Remains only testing and i need relook it all before release. I think, at today, or at tomorrow i will release v2.

Awesome, sounds good!

One more problem has been solved in v2: Renaming of user identities. In old: if username changed - on subordinated service will create duplicate with new username.

When do you think the rest will be done?

I'm glad to tell you... @kakulukia ...

Sweet! :)

Now i have a longer todo for this weekend. :(