python to ldap two-ways serialization

This commit is contained in:
Éloi Rivard 2021-12-08 15:53:20 +01:00
parent 015d410fb6
commit 65dd61c524
8 changed files with 79 additions and 64 deletions

View file

@ -142,7 +142,7 @@ def add(user):
return render_template("admin/client_add.html", form=form, menuitem="admin") return render_template("admin/client_add.html", form=form, menuitem="admin")
client_id = gen_salt(24) client_id = gen_salt(24)
client_id_issued_at = datetime.datetime.now().strftime("%Y%m%d%H%M%SZ") client_id_issued_at = datetime.datetime.now()
client = Client( client = Client(
oauthClientID=client_id, oauthClientID=client_id,
oauthIssueDate=client_id_issued_at, oauthIssueDate=client_id_issued_at,
@ -161,7 +161,7 @@ def add(user):
oauthSoftwareVersion=form["oauthSoftwareVersion"].data, oauthSoftwareVersion=form["oauthSoftwareVersion"].data,
oauthJWK=form["oauthJWK"].data, oauthJWK=form["oauthJWK"].data,
oauthJWKURI=form["oauthJWKURI"].data, oauthJWKURI=form["oauthJWKURI"].data,
oauthPreconsent="TRUE" if form["oauthPreconsent"].data else "FALSE", oauthPreconsent=form["oauthPreconsent"].data,
oauthClientSecret="" oauthClientSecret=""
if form["oauthTokenEndpointAuthMethod"].data == "none" if form["oauthTokenEndpointAuthMethod"].data == "none"
else gen_salt(48), else gen_salt(48),
@ -225,7 +225,7 @@ def client_edit(client_id):
oauthJWK=form["oauthJWK"].data, oauthJWK=form["oauthJWK"].data,
oauthJWKURI=form["oauthJWKURI"].data, oauthJWKURI=form["oauthJWKURI"].data,
oauthAudience=form["oauthAudience"].data, oauthAudience=form["oauthAudience"].data,
oauthPreconsent="TRUE" if form["oauthPreconsent"].data else "FALSE", oauthPreconsent=form["oauthPreconsent"].data,
) )
client.save() client.save()
flash( flash(

View file

@ -1,5 +1,7 @@
import datetime
import ldap import ldap
import ldap.filter import ldap.filter
import warnings
from flask import g from flask import g
@ -153,20 +155,48 @@ class LDAPObject:
return cls._attribute_type_by_name return cls._attribute_type_by_name
@staticmethod
def ldap_to_python(name, value):
syntax = LDAPObject.ldap_object_attributes()[name].syntax
if syntax == "1.3.6.1.4.1.1466.115.121.1.24": # Generalized Time
value = value.decode("utf-8")
return datetime.datetime.strptime(value, "%Y%m%d%H%M%SZ") if value else None
if syntax == "1.3.6.1.4.1.1466.115.121.1.27": # Integer
return int(value.decode("utf-8"))
if syntax == "1.3.6.1.4.1.1466.115.121.1.7": # Boolean
return value.decode("utf-8").upper() == "TRUE"
return value.decode("utf-8")
@staticmethod
def python_to_ldap(name, value):
syntax = LDAPObject.ldap_object_attributes()[name].syntax
if syntax == "1.3.6.1.4.1.1466.115.121.1.24": # Generalized Time
return value.strftime("%Y%m%d%H%M%SZ").encode("utf-8")
if syntax == "1.3.6.1.4.1.1466.115.121.1.27": # Integer
return str(value).encode("utf-8")
if syntax == "1.3.6.1.4.1.1466.115.121.1.7": # Boolean
return ("TRUE" if value else "FALSE").encode("utf-8")
return value.encode("utf-8")
@staticmethod @staticmethod
def ldap_attrs_to_python(attrs): def ldap_attrs_to_python(attrs):
return { return {
name: [value.decode("utf-8") for value in values] name: [LDAPObject.ldap_to_python(name, value) for value in values]
for name, values in attrs.items() for name, values in attrs.items()
} }
@staticmethod @staticmethod
def python_attrs_to_ldap(attrs): def python_attrs_to_ldap(attrs):
return { return {
name: [ name: [LDAPObject.python_to_ldap(name, value) for value in values]
value.encode("utf-8") if isinstance(value, str) else value
for value in values
]
for name, values in attrs.items() for name, values in attrs.items()
} }
@ -190,9 +220,10 @@ class LDAPObject:
for name, value in self.changes.items() for name, value in self.changes.items()
if value and value[0] and self.attrs.get(name) != value if value and value[0] and self.attrs.get(name) != value
} }
changes = self.python_attrs_to_ldap(changes) formatted_changes = self.python_attrs_to_ldap(changes)
modlist = [ modlist = [
(ldap.MOD_REPLACE, name, values) for name, values in changes.items() (ldap.MOD_REPLACE, name, values)
for name, values in formatted_changes.items()
] ]
conn.modify_s(self.dn, modlist) conn.modify_s(self.dn, modlist)
@ -203,8 +234,8 @@ class LDAPObject:
for name, value in {**self.attrs, **self.changes}.items() for name, value in {**self.attrs, **self.changes}.items()
if value and value[0] if value and value[0]
} }
changes = self.python_attrs_to_ldap(changes) formatted_changes = self.python_attrs_to_ldap(changes)
attributes = [(name, values) for name, values in changes.items()] attributes = [(name, values) for name, values in formatted_changes.items()]
conn.add_s(self.dn, attributes) conn.add_s(self.dn, attributes)
self.attrs = {**self.attrs, **self.changes} self.attrs = {**self.attrs, **self.changes}

View file

@ -215,15 +215,11 @@ class Client(LDAPObject, ClientMixin):
@property @property
def issue_date(self): def issue_date(self):
return ( return self.oauthIssueDate
datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ")
if self.oauthIssueDate
else None
)
@property @property
def preconsent(self): def preconsent(self):
return self.oauthPreconsent and self.oauthPreconsent.lower() == "true" return self.oauthPreconsent
def get_client_id(self): def get_client_id(self):
return self.oauthClientID return self.oauthClientID
@ -269,11 +265,7 @@ class AuthorizationCode(LDAPObject, AuthorizationCodeMixin):
@property @property
def issue_date(self): def issue_date(self):
return ( return self.oauthIssueDate
datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ")
if self.oauthIssueDate
else None
)
def get_redirect_uri(self): def get_redirect_uri(self):
return self.oauthRedirectURI return self.oauthRedirectURI
@ -286,16 +278,17 @@ class AuthorizationCode(LDAPObject, AuthorizationCodeMixin):
def is_expired(self): def is_expired(self):
return ( return (
datetime.datetime.strptime(self.oauthAuthorizationDate, "%Y%m%d%H%M%SZ") self.oauthAuthorizationDate
+ datetime.timedelta(seconds=int(self.oauthAuthorizationLifetime)) + datetime.timedelta(seconds=int(self.oauthAuthorizationLifetime))
< datetime.datetime.now() < datetime.datetime.now()
) )
def get_auth_time(self): def get_auth_time(self):
auth_time = datetime.datetime.strptime( return int(
self.oauthAuthorizationDate, "%Y%m%d%H%M%SZ" (
self.oauthAuthorizationDate - datetime.datetime(1970, 1, 1)
).total_seconds()
) )
return int((auth_time - datetime.datetime(1970, 1, 1)).total_seconds())
class Token(LDAPObject, TokenMixin): class Token(LDAPObject, TokenMixin):
@ -305,17 +298,13 @@ class Token(LDAPObject, TokenMixin):
@property @property
def issue_date(self): def issue_date(self):
return ( return self.oauthIssueDate
datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ")
if self.oauthIssueDate
else None
)
@property @property
def expire_date(self): def expire_date(self):
return datetime.datetime.strptime( return self.oauthIssueDate + datetime.timedelta(
self.oauthIssueDate, "%Y%m%d%H%M%SZ" seconds=int(self.oauthTokenLifetime)
) + datetime.timedelta(seconds=int(self.oauthTokenLifetime)) )
@property @property
def revoked(self): def revoked(self):
@ -331,12 +320,14 @@ class Token(LDAPObject, TokenMixin):
return int(self.oauthTokenLifetime) return int(self.oauthTokenLifetime)
def get_issued_at(self): def get_issued_at(self):
issue_date = datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ") return int(
return int((issue_date - datetime.datetime(1970, 1, 1)).total_seconds()) (self.oauthIssueDate - datetime.datetime(1970, 1, 1)).total_seconds()
)
def get_expires_at(self): def get_expires_at(self):
issue_date = datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ") issue_timestamp = (
issue_timestamp = (issue_date - datetime.datetime(1970, 1, 1)).total_seconds() self.oauthIssueDate - datetime.datetime(1970, 1, 1)
).total_seconds()
return int(issue_timestamp) + int(self.oauthTokenLifetime) return int(issue_timestamp) + int(self.oauthTokenLifetime)
def is_refresh_token_active(self): def is_refresh_token_active(self):
@ -347,7 +338,7 @@ class Token(LDAPObject, TokenMixin):
def is_expired(self): def is_expired(self):
return ( return (
datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ") self.oauthIssueDate
+ datetime.timedelta(seconds=int(self.oauthTokenLifetime)) + datetime.timedelta(seconds=int(self.oauthTokenLifetime))
< datetime.datetime.now() < datetime.datetime.now()
) )
@ -366,18 +357,14 @@ class Consent(LDAPObject):
@property @property
def issue_date(self): def issue_date(self):
return ( return self.oauthIssueDate
datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ")
if self.oauthIssueDate
else None
)
@property @property
def revokation_date(self): def revokation_date(self):
return datetime.datetime.strptime(self.oauthRevokationDate, "%Y%m%d%H%M%SZ") return self.oauthRevokationDate
def revoke(self): def revoke(self):
self.oauthRevokationDate = datetime.datetime.now().strftime("%Y%m%d%H%M%SZ") self.oauthRevokationDate = datetime.datetime.now()
self.save() self.save()
tokens = Token.filter( tokens = Token.filter(

View file

@ -136,7 +136,7 @@ def authorize():
oauthClient=client.dn, oauthClient=client.dn,
oauthSubject=user.dn, oauthSubject=user.dn,
oauthScope=scopes, oauthScope=scopes,
oauthIssueDate=datetime.datetime.now().strftime("%Y%m%d%H%M%SZ"), oauthIssueDate=datetime.datetime.now(),
) )
consent.save() consent.save()

View file

@ -88,7 +88,7 @@ def save_authorization_code(code, request):
oauthRedirectURI=request.redirect_uri or request.client.oauthRedirectURIs[0], oauthRedirectURI=request.redirect_uri or request.client.oauthRedirectURIs[0],
oauthScope=request.scope, oauthScope=request.scope,
oauthNonce=nonce, oauthNonce=nonce,
oauthAuthorizationDate=now.strftime("%Y%m%d%H%M%SZ"), oauthAuthorizationDate=now,
oauthAuthorizationLifetime=str(84000), oauthAuthorizationLifetime=str(84000),
oauthCodeChallenge=request.data.get("code_challenge"), oauthCodeChallenge=request.data.get("code_challenge"),
oauthCodeChallengeMethod=request.data.get("code_challenge_method"), oauthCodeChallengeMethod=request.data.get("code_challenge_method"),
@ -153,9 +153,7 @@ class RefreshTokenGrant(_RefreshTokenGrant):
return user.dn return user.dn
def revoke_old_credential(self, credential): def revoke_old_credential(self, credential):
credential.oauthRevokationDate = datetime.datetime.now().strftime( credential.oauthRevokationDate = datetime.datetime.now()
"%Y%m%d%H%M%SZ"
)
credential.save() credential.save()
@ -201,7 +199,7 @@ def save_token(token, request):
t = Token( t = Token(
oauthTokenType=token["token_type"], oauthTokenType=token["token_type"],
oauthAccessToken=token["access_token"], oauthAccessToken=token["access_token"],
oauthIssueDate=now.strftime("%Y%m%d%H%M%SZ"), oauthIssueDate=now,
oauthTokenLifetime=str(token["expires_in"]), oauthTokenLifetime=str(token["expires_in"]),
oauthScope=token["scope"], oauthScope=token["scope"],
oauthClient=request.client.dn, oauthClient=request.client.dn,
@ -241,7 +239,7 @@ class RevocationEndpoint(_RevocationEndpoint):
return None return None
def revoke_token(self, token): def revoke_token(self, token):
token.oauthRevokationDate = datetime.datetime.now().strftime("%Y%m%d%H%M%SZ") token.oauthRevokationDate = datetime.datetime.now()
token.save() token.save()

View file

@ -16,7 +16,7 @@ def test_clean_command(testclient, slapd_connection, client, user):
oauthNonce="nonce", oauthNonce="nonce",
oauthAuthorizationDate=( oauthAuthorizationDate=(
datetime.datetime.now() - datetime.timedelta(days=1) datetime.datetime.now() - datetime.timedelta(days=1)
).strftime("%Y%m%d%H%M%SZ"), ),
oauthAuthorizationLifetime="3600", oauthAuthorizationLifetime="3600",
oauthCodeChallenge="challenge", oauthCodeChallenge="challenge",
oauthCodeChallengeMethod="method", oauthCodeChallengeMethod="method",
@ -32,9 +32,7 @@ def test_clean_command(testclient, slapd_connection, client, user):
oauthTokenType=None, oauthTokenType=None,
oauthRefreshToken=gen_salt(48), oauthRefreshToken=gen_salt(48),
oauthScope="openid profile", oauthScope="openid profile",
oauthIssueDate=(datetime.datetime.now() - datetime.timedelta(days=1)).strftime( oauthIssueDate=(datetime.datetime.now() - datetime.timedelta(days=1)),
"%Y%m%d%H%M%SZ"
),
oauthTokenLifetime=str(3600), oauthTokenLifetime=str(3600),
) )
token.save(slapd_connection) token.save(slapd_connection)

View file

@ -234,7 +234,7 @@ def client(app, slapd_connection, other_client):
"https://mydomain.tld/redirect2", "https://mydomain.tld/redirect2",
], ],
oauthLogoURI="https://mydomain.tld/logo.png", oauthLogoURI="https://mydomain.tld/logo.png",
oauthIssueDate=datetime.datetime.now().strftime("%Y%m%d%H%S%MZ"), oauthIssueDate=datetime.datetime.now(),
oauthClientSecret=gen_salt(48), oauthClientSecret=gen_salt(48),
oauthGrantType=[ oauthGrantType=[
"password", "password",
@ -268,7 +268,7 @@ def other_client(app, slapd_connection):
"https://myotherdomain.tld/redirect2", "https://myotherdomain.tld/redirect2",
], ],
oauthLogoURI="https://myotherdomain.tld/logo.png", oauthLogoURI="https://myotherdomain.tld/logo.png",
oauthIssueDate=datetime.datetime.now().strftime("%Y%m%d%H%S%MZ"), oauthIssueDate=datetime.datetime.now(),
oauthClientSecret=gen_salt(48), oauthClientSecret=gen_salt(48),
oauthGrantType=[ oauthGrantType=[
"password", "password",
@ -300,7 +300,7 @@ def authorization(app, slapd_connection, user, client):
oauthResponseType="code", oauthResponseType="code",
oauthScope="openid profile", oauthScope="openid profile",
oauthNonce="nonce", oauthNonce="nonce",
oauthAuthorizationDate="20200101000000Z", oauthAuthorizationDate=datetime.datetime(2020, 1, 1),
oauthAuthorizationLifetime="3600", oauthAuthorizationLifetime="3600",
oauthCodeChallenge="challenge", oauthCodeChallenge="challenge",
oauthCodeChallengeMethod="method", oauthCodeChallengeMethod="method",
@ -313,6 +313,7 @@ def authorization(app, slapd_connection, user, client):
@pytest.fixture @pytest.fixture
def user(app, slapd_connection): def user(app, slapd_connection):
User.ldap_object_classes(slapd_connection) User.ldap_object_classes(slapd_connection)
LDAPObject.ldap_object_attributes(slapd_connection)
u = User( u = User(
objectClass=["inetOrgPerson"], objectClass=["inetOrgPerson"],
cn="John (johnny) Doe", cn="John (johnny) Doe",
@ -365,7 +366,7 @@ def token(slapd_connection, client, user):
oauthTokenType=None, oauthTokenType=None,
oauthRefreshToken=gen_salt(48), oauthRefreshToken=gen_salt(48),
oauthScope="openid profile", oauthScope="openid profile",
oauthIssueDate=datetime.datetime.now().strftime("%Y%m%d%H%M%SZ"), oauthIssueDate=datetime.datetime.now(),
oauthTokenLifetime=str(3600), oauthTokenLifetime=str(3600),
) )
t.save(slapd_connection) t.save(slapd_connection)
@ -378,7 +379,7 @@ def consent(slapd_connection, client, user):
oauthClient=client.dn, oauthClient=client.dn,
oauthSubject=user.dn, oauthSubject=user.dn,
oauthScope=["openid", "profile"], oauthScope=["openid", "profile"],
oauthIssueDate=datetime.datetime.now().strftime("%Y%m%d%H%M%SZ"), oauthIssueDate=datetime.datetime.now(),
) )
t.save(slapd_connection) t.save(slapd_connection)
return t return t

View file

@ -67,7 +67,7 @@ def test_authorization_code_flow(
def test_authorization_code_flow_preconsented( def test_authorization_code_flow_preconsented(
testclient, slapd_connection, logged_user, client, keypair, other_client testclient, slapd_connection, logged_user, client, keypair, other_client
): ):
client.oauthPreconsent = "TRUE" client.oauthPreconsent = True
client.save(conn=slapd_connection) client.save(conn=slapd_connection)
res = testclient.get( res = testclient.get(