Commit 0df7203c authored by Lukas Burgey's avatar Lukas Burgey
Browse files

Enhance the API usability

parent 1d8eac79
...@@ -68,11 +68,11 @@ def default_idp(): ...@@ -68,11 +68,11 @@ def default_idp():
class OIDCTokenAuthBackend(object): class OIDCTokenAuthBackend(object):
def get_user_info(self, request, access_token, token_type='Bearer'): def get_userinfo(self, request, access_token, token_type='Bearer'):
idp_id = utils.get_session(request, 'idp_id', None) idp_id = utils.get_session(request, 'idp_id', None)
oidc_config = OIDCConfig.objects.get(id=idp_id) idp = OIDCConfig.objects.get(id=idp_id)
req = Request( req = Request(
oidc_config.oidc_client.provider_info['userinfo_endpoint'] idp.oidc_client.provider_info['userinfo_endpoint']
) )
auth = (token_type + ' ' + access_token) auth = (token_type + ' ' + access_token)
req.add_header('Authorization', auth) req.add_header('Authorization', auth)
...@@ -85,24 +85,15 @@ class OIDCTokenAuthBackend(object): ...@@ -85,24 +85,15 @@ class OIDCTokenAuthBackend(object):
return None return None
# get the user info from the idp # get the user info from the idp
user_info = self.get_user_info(request, token) userinfo = self.get_userinfo(request, token)
idp_id = utils.get_session(request, 'idp_id', None) idp_id = utils.get_session(request, 'idp_id', None)
oidc_config = OIDCConfig.objects.get(id=idp_id) idp = OIDCConfig.objects.get(id=idp_id)
try: try:
# if we know the user we return him return models.User.get_user(
return oidc_config.users.get( userinfo,
sub=user_info['sub'] idp,
) )
except ObjectDoesNotExist:
try:
# if we do not know the user yet, we create him
user = models.User.construct_from_user_info(
user_info,
oidc_config,
)
user.save()
return user
except Exception as exception: except Exception as exception:
LOGGER.error('OIDCTokenAuthBackend: error constructing user: %s', exception) LOGGER.error('OIDCTokenAuthBackend: error constructing user: %s', exception)
...@@ -111,6 +102,7 @@ class OIDCTokenAuthBackend(object): ...@@ -111,6 +102,7 @@ class OIDCTokenAuthBackend(object):
def get_user(self, user_id): def get_user(self, user_id):
try: try:
return models.User.objects.get( return models.User.objects.get(
user_type='oidcuser',
pk=user_id pk=user_id
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
......
...@@ -107,19 +107,10 @@ class DeploymentView(views.APIView): ...@@ -107,19 +107,10 @@ class DeploymentView(views.APIView):
id=request.data['key'], id=request.data['key'],
) )
# check if there is already an deployment deployment = models.Deployment.get_deployment(
# if not request.user.deployments.filter(service=request_service).exists(): request.user,
try: request_service,
deployment = request.user.deployments.get(service=request_service)
except ObjectDoesNotExist:
if request_type == 'remove':
return _api_error_response()
deployment = models.Deployment(
user=request.user,
service=request_service,
) )
deployment.save()
if request_type == 'add': if request_type == 'add':
deployment.deploy_key(request_key) deployment.deploy_key(request_key)
......
...@@ -177,6 +177,63 @@ class User(AbstractUser): ...@@ -177,6 +177,63 @@ class User(AbstractUser):
editable=False, editable=False,
) )
# returns the user as identified by userinfo and idp
# if the user does not exists
@classmethod
def get_user(cls, userinfo, idp):
if not 'sub' in userinfo:
raise Exception('get_user needs a userinfo which contains the users subject')
query_result = cls.objects.filter(
sub=userinfo['sub'],
idp=idp,
)
if not query_result.exists():
return cls.construct_from_userinfo(userinfo, idp)
if len(query_result) > 1:
return Exception('Two user instances with same subject from the same idp')
# TODO update the users userinfo when it changes
# TODO update the users groupinfo when it changes
return query_result.first()
@classmethod
def construct_from_userinfo(cls, userinfo, idp):
LOGGER.debug('User: constructing from %s', userinfo)
if 'sub' not in userinfo:
raise Exception('Missing attribute in userinfo: sub')
sub = userinfo['sub']
if 'email' not in userinfo:
if 'name' not in userinfo:
raise Exception('Missing attributes in userinfo: email and name')
username = userinfo['name']
else:
username = userinfo['email']
user = cls(
user_type='oidcuser',
username=username,
sub=sub,
idp=idp,
userinfo=userinfo,
)
user.save()
return user
@classmethod
def construct_client(cls, username, password):
LOGGER.debug('APICLIENT: new client %s', username)
client = cls(
username=username,
user_type='apiclient',
)
client.set_password(password)
return client
# we hide deleted keys here # we hide deleted keys here
# the full list of ssh keys is self._ssh_keys # the full list of ssh keys is self._ssh_keys
@property @property
...@@ -222,55 +279,32 @@ class User(AbstractUser): ...@@ -222,55 +279,32 @@ class User(AbstractUser):
LOGGER.error(self.msg('already activated')) LOGGER.error(self.msg('already activated'))
return return
if self.user_type == 'oidcuser':
self.is_active = True self.is_active = True
self._is_active = True self._is_active = True
self.save() self.save()
LOGGER.info(self.msg('activated'))
# oidcuser: deploy the according credentials
if self.user_type == 'oidcuser':
for dep in self.deployments.all(): for dep in self.deployments.all():
dep.activate() dep.activate()
LOGGER.info(self.msg('activated'))
# oidcuser: withdraw all credentials
def deactivate(self): def deactivate(self):
if not self._is_active: if not self._is_active:
LOGGER.error(self.msg('already deactivated')) LOGGER.error(self.msg('already deactivated'))
return return
if self.user_type == 'oidcuser':
self.is_active = False self.is_active = False
self._is_active = False self._is_active = False
self.save() self.save()
for dep in self.deployments.all():
dep.deactivate()
LOGGER.info(self.msg('deactivated')) LOGGER.info(self.msg('deactivated'))
# oidcuser: withdraw all credentials
if self.user_type == 'oidcuser':
@classmethod for dep in self.deployments.all():
def construct_from_user_info(cls, user_info, idp): dep.deactivate()
LOGGER.debug('User: constructing from %s', user_info)
return cls(
sub=user_info.get('sub', ''),
first_name=user_info.get('given_name', ''),
last_name=user_info.get('family_name', ''),
email=user_info.get('email', ''),
username=user_info.get('email', ''),
idp=idp,
userinfo=user_info,
)
@classmethod
def construct_client(cls, username, password):
LOGGER.debug('APICLIENT: new client %s', username)
client = cls(
username=username,
user_type='apiclient',
)
client.set_password(password)
return client
class Site(models.Model): class Site(models.Model):
...@@ -408,6 +442,27 @@ class Deployment(models.Model): ...@@ -408,6 +442,27 @@ class Deployment(models.Model):
default=True, default=True,
) )
# get a deployment for a user/service.
# if it does not exist it is created
@classmethod
def get_deployment(cls, user, service):
query = cls.objects.filter(
user=user,
).filter(
service=service,
)
if query.exists():
return query.first()
deployment = cls(
user=user,
service=service,
)
deployment.save()
return deployment
@property @property
def withdrawals(self): def withdrawals(self):
return self.tasks.filter(action='withdraw') return self.tasks.filter(action='withdraw')
...@@ -452,55 +507,18 @@ class Deployment(models.Model): ...@@ -452,55 +507,18 @@ class Deployment(models.Model):
# only deploy the key # only deploy the key
def _deploy_key(self, key): def _deploy_key(self, key):
# delete outstanding tasks which are made obsolete by this task task = DeploymentTask.construct_deployment_task(
for withdrawal in self.withdrawals.filter(key=key):
LOGGER.debug(withdrawal.msg('now obsolete'))
withdrawal.delete()
# generate task
task = DeploymentTask(
action='deploy',
deployment=self, deployment=self,
key=key, key=key,
) )
task.save()
LOGGER.debug(task.msg('generated'))
# generate task items
for site in self.service.site.all():
deploy = DeploymentTaskItem(
task=task,
site=site,
)
deploy.save()
LOGGER.debug(deploy.msg('generated'))
# publish the task # publish the task
task.publish() task.publish()
def _withdraw_key(self, key): def _withdraw_key(self, key):
# delete outstanding tasks which are made obsolete by this task task = DeploymentTask.construct_withdrawal_task(
for deploy in self.deploys.filter(key=key):
LOGGER.debug(deploy.msg("now obsolete"))
deploy.delete()
# generate task
task = DeploymentTask(
action='withdraw',
deployment=self, deployment=self,
key=key, key=key,
) )
task.save()
LOGGER.debug(task.msg('generated'))
# generate task items
for site in self.service.site.all():
withdrawal = DeploymentTaskItem(
task=task,
site=site,
)
withdrawal.save()
LOGGER.debug(withdrawal.msg('generated'))
# publish the task # publish the task
task.publish() task.publish()
...@@ -554,6 +572,58 @@ class DeploymentTask(models.Model): ...@@ -554,6 +572,58 @@ class DeploymentTask(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
@classmethod
def construct_deployment_task(cls, deployment, key):
# delete outstanding tasks which are made obsolete by this task
for withdrawal in deployment.withdrawals.filter(key=key):
LOGGER.debug(withdrawal.msg('now obsolete'))
withdrawal.delete()
task = cls(
action='deploy',
deployment=deployment,
key=key,
)
task.save()
LOGGER.debug(task.msg('generated'))
# generate task items
for site in deployment.service.site.all():
deploy = DeploymentTaskItem(
task=task,
site=site,
)
deploy.save()
LOGGER.debug(deploy.msg('generated'))
return task
@classmethod
def construct_withdrawal_task(cls, deployment, key):
# delete outstanding tasks which are made obsolete by this task
for withdrawal in deployment.deploys.filter(key=key):
LOGGER.debug(withdrawal.msg('now obsolete'))
withdrawal.delete()
task = cls(
action='withdraw',
deployment=deployment,
key=key,
)
task.save()
LOGGER.debug(task.msg('generated'))
# generate task items
for site in deployment.service.site.all():
deploy = DeploymentTaskItem(
task=task,
site=site,
)
deploy.save()
LOGGER.debug(deploy.msg('generated'))
return task
@property @property
def user(self): def user(self):
return self.deployment.user return self.deployment.user
...@@ -630,16 +700,6 @@ class DeploymentTaskItem(models.Model): ...@@ -630,16 +700,6 @@ class DeploymentTaskItem(models.Model):
# RECEIVERS # RECEIVERS
# #
@receiver(post_save, sender=User)
def upgrade_password(sender, instance=None, created=False, **kwargs):
if (
instance is not None
and instance.user_type == 'apiclient'
and not instance.password.startswith('pbkdf2_sha256')
):
instance.set_password(instance.password)
instance.save()
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def deactivate_user(sender, instance=None, created=False, **kwargs): def deactivate_user(sender, instance=None, created=False, **kwargs):
if created: if created:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment