Skip to content
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

Filtered WrapSpawner #15

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ configuration of Spawner classes while permitting:
* configuration of Spawner classes that don't natively implement `options_form`
* administrator control of allowed configuration changes
* runtime choice of which Spawner backend to launch

FilteredSpawner allows the definition of profiles that can be assigned only to authenticated users belonging to a specific set of groups.Based over the original ProfilesSpawner it exploits a specific additional configuration profile parameter to specify the set of allowed groups or a * wildcard for enabling that specific profile for all the users


### Example

Expand Down Expand Up @@ -72,6 +75,29 @@ running as a local process or one of two different Docker Images to run within `
]
```

<b>FilteredSpawner</b> example
```python
c.JupyterHub.spawner_class = 'wrapspawner.FilteredSpawner'
c.Spawner.http_timeout = 120
#------------------------------------------------------------------------------
# ProfilesSpawner configuration
#------------------------------------------------------------------------------
# List of profiles to offer for selection. Signature is:
# List(Tuple( Unicode, Unicode, Type(Spawner), Dict, Unicode ))
# corresponding to profile display name, unique key, Spawner class,
# dictionary of spawner config options, comma separated list of authorized groups..
#
# The first three values will be exposed in the input_template as {display},
# {key}, and {type}
#
c.ProfilesSpawner.default_profiles = [
( "Sudospawner group1", 'sudospawner', 'sudospawner.SudoSpawner', {'cmd':['sudospawner-singleuser'], 'notebook_dir':''}, 'group1' ),
( "Sudospawner group2", 'sudospawner', 'sudospawner.SudoSpawner', {'cmd':['sudospawner-singleuser'], 'notebook_dir':''}, 'group2' ),
( "Global spawner", 'sudospawner', 'sudospawner.SudoSpawner', {'cmd':['sudospawner-singleuser'], 'notebook_dir':''}, '*' )

]
```

## History

These mechanisms originated as part of the [`batchspawner`](https://github.com/jupyterhub/batchspawner) package.
Expand Down
51 changes: 50 additions & 1 deletion wrapspawner/wrapspawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ class ProfilesSpawner(WrapSpawner):
help = """List of profiles to offer for selection. Signature is:
List(Tuple( Unicode, Unicode, Type(Spawner), Dict )) corresponding to
profile display name, unique key, Spawner class, dictionary of spawner config options.

The first three values will be exposed in the input_template as {display}, {key}, and {type}"""
)

Expand Down Expand Up @@ -220,6 +219,56 @@ def clear_state(self):
super().clear_state()
self.child_profile = ''

class FilteredSpawner(ProfilesSpawner):

"""ProfilesSpawner - leverages the Spawner options form feature to allow user-driven
configuration of Spawner classes while permitting:
1) configuration of Spawner classes that don't natively implement options_form
2) administrator control of allowed configuration changes
3) runtime choice of which Spawner backend to launch
"""
default_profiles = List(
trait = Tuple( Unicode(), Unicode(), Type(Spawner), Dict(), Unicode() ),
default_value = [],
config = True,
help = """List of profiles to offer in addition to docker images for selection. Signature is:
List(Tuple( Unicode, Unicode, Type(Spawner), Dict , Unicode)) corresponding to
profile display name, unique key, Spawner class, dictionary of spawner config options, comma separated list of authorized groups.
The first three values will be exposed in the input_template as {display}, {key}, and {type}"""
)

def get_user_groups(self):
import subprocess, sys
user = self.user.name
cmd_result = subprocess.Popen("groups "+user, shell=True, stdout=subprocess.PIPE).stdout.read()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a safe or generic way to get groups. It assumes users are local on the Hub system, which is not generally true.

#Get system default encoding
sys_encoding = sys.getdefaultencoding()
groups_list = cmd_result.decode(sys_encoding).split(':')[1].replace('\n', '').strip().split(' ')
return groups_list

def _user_profiles(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than implementing logic specific to your use case (filtering based on OS groups), maybe this filter should itself be a hook and then your configuration would register the appropriate filter function for your use case?

#Parse default_profiles and return a valid list
valid_profiles = []
for p in self.default_profiles:
#Parse authorized groups
#If authorized_groups contained the all wildcard, pass it without groups check
if p[4] == '*':
valid_profiles.append((p[0], p[1], p[2], p[3]))
#Check if profile is enabled for the user
authorized_groups = p[4].split(',')
user_groups = self.get_user_groups()
#Interesection between groups
groups_intersection = list(set(authorized_groups).intersection(user_groups))
#If groups_intersection is not empty, profile is authorized for user
if groups_intersection is not None and len(groups_intersection)>0:
valid_profiles.append((p[0], p[1], p[2], p[3]))
return valid_profiles

@property
def profiles(self):
#New profiles are default profiles in addition to user_based profiles
return self._user_profiles()

class DockerProfilesSpawner(ProfilesSpawner):

"""DockerProfilesSpawner - leverages ProfilesSpawner to dynamically create DockerSpawner
Expand Down