135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
|
"""passbook core models"""
|
||
|
import re
|
||
|
from logging import getLogger
|
||
|
|
||
|
import reversion
|
||
|
from django.contrib.auth.models import AbstractUser
|
||
|
from django.db import models
|
||
|
|
||
|
from passbook.lib.models import CastableModel, CreatedUpdatedModel
|
||
|
|
||
|
LOGGER = getLogger(__name__)
|
||
|
|
||
|
@reversion.register()
|
||
|
class User(AbstractUser):
|
||
|
"""Custom User model to allow easier adding o f user-based settings"""
|
||
|
|
||
|
sources = models.ManyToManyField('Source', through='UserSourceConnection')
|
||
|
|
||
|
@reversion.register()
|
||
|
class Application(CastableModel, CreatedUpdatedModel):
|
||
|
"""Every Application which uses passbook for authentication/identification/authorization
|
||
|
needs an Application record. Other authentication types can subclass this Model to
|
||
|
add custom fields and other properties"""
|
||
|
|
||
|
name = models.TextField()
|
||
|
launch_url = models.URLField(null=True, blank=True)
|
||
|
icon_url = models.TextField(null=True, blank=True)
|
||
|
|
||
|
def user_is_authorized(self, user: User) -> bool:
|
||
|
"""Check if user is authorized to use this application"""
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.name
|
||
|
|
||
|
@reversion.register()
|
||
|
class Source(CastableModel, CreatedUpdatedModel):
|
||
|
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||
|
|
||
|
name = models.TextField()
|
||
|
slug = models.SlugField()
|
||
|
form = None # ModelForm-based class ued to create/edit instance
|
||
|
enabled = models.BooleanField(default=True)
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.name
|
||
|
|
||
|
@reversion.register()
|
||
|
class UserSourceConnection(CreatedUpdatedModel):
|
||
|
"""Connection between User and Source."""
|
||
|
|
||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||
|
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||
|
|
||
|
class Meta:
|
||
|
|
||
|
unique_together = (('user', 'source'),)
|
||
|
|
||
|
@reversion.register()
|
||
|
class Rule(CastableModel, CreatedUpdatedModel):
|
||
|
"""Rules which specify if a user is authorized to use an Application. Can be overridden by
|
||
|
other types to add other fields, more logic, etc."""
|
||
|
|
||
|
ACTION_ALLOW = 'allow'
|
||
|
ACTION_DENY = 'deny'
|
||
|
ACTIONS = (
|
||
|
(ACTION_ALLOW, ACTION_ALLOW),
|
||
|
(ACTION_DENY, ACTION_DENY),
|
||
|
)
|
||
|
|
||
|
name = models.TextField(blank=True, null=True)
|
||
|
application = models.ForeignKey(Application, on_delete=models.CASCADE)
|
||
|
action = models.CharField(max_length=20, choices=ACTIONS)
|
||
|
negate = models.BooleanField(default=False)
|
||
|
|
||
|
def __str__(self):
|
||
|
if self.name:
|
||
|
return self.name
|
||
|
return "%s action %s" % (self.application, self.action)
|
||
|
|
||
|
def user_passes(self, user: User) -> bool:
|
||
|
"""Check if user instance passes this rule"""
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
@reversion.register()
|
||
|
class FieldMatcherRule(Rule):
|
||
|
"""Rule which checks if a field of the User model matches/doesn't match a
|
||
|
certain pattern"""
|
||
|
|
||
|
MATCH_STARTSWITH = 'startswith'
|
||
|
MATCH_ENDSWITH = 'endswith'
|
||
|
MATCH_CONTAINS = 'contains'
|
||
|
MATCH_REGEXP = 'regexp'
|
||
|
MATCH_EXACT = 'exact'
|
||
|
MATCHES = (
|
||
|
(MATCH_STARTSWITH, MATCH_STARTSWITH),
|
||
|
(MATCH_ENDSWITH, MATCH_ENDSWITH),
|
||
|
(MATCH_ENDSWITH, MATCH_CONTAINS),
|
||
|
(MATCH_REGEXP, MATCH_REGEXP),
|
||
|
(MATCH_EXACT, MATCH_EXACT),
|
||
|
)
|
||
|
|
||
|
user_field = models.TextField()
|
||
|
match_action = models.CharField(max_length=50, choices=MATCHES)
|
||
|
value = models.TextField()
|
||
|
|
||
|
def __str__(self):
|
||
|
description = "app %s, user.%s %s '%s'" % (self.application, self.user_field,
|
||
|
self.match_action, self.value)
|
||
|
if self.name:
|
||
|
description = "%s: %s" % (self.name, description)
|
||
|
return description
|
||
|
|
||
|
def user_passes(self, user: User) -> bool:
|
||
|
"""Check if user instance passes this role"""
|
||
|
if not hasattr(user, self.user_field):
|
||
|
raise ValueError("Field does not exist")
|
||
|
user_field_value = getattr(user, self.user_field, None)
|
||
|
LOGGER.debug("Checked '%s' %s with '%s'...",
|
||
|
user_field_value, self.match_action, self.value)
|
||
|
passes = False
|
||
|
if self.match_action == FieldMatcherRule.MATCH_STARTSWITH:
|
||
|
passes = user_field_value.startswith(self.value)
|
||
|
if self.match_action == FieldMatcherRule.MATCH_ENDSWITH:
|
||
|
passes = user_field_value.endswith(self.value)
|
||
|
if self.match_action == FieldMatcherRule.MATCH_CONTAINS:
|
||
|
passes = self.value in user_field_value
|
||
|
if self.match_action == FieldMatcherRule.MATCH_REGEXP:
|
||
|
pattern = re.compile(self.value)
|
||
|
passes = pattern.match(user_field_value)
|
||
|
if self.negate:
|
||
|
passes = not passes
|
||
|
LOGGER.debug("User got '%r'", passes)
|
||
|
return passes
|