Initial commit: OCA Server Auth packages (29 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:06 +02:00
commit 3ed80311c4
1325 changed files with 127292 additions and 0 deletions

View file

@ -0,0 +1 @@
from . import fake_idp, test_pysaml

View file

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID7TCCAtWgAwIBAgIUDBX/LJ1BPZOhb2vrDnwIasyEi+AwDQYJKoZIhvcNAQEL
BQAwgYUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMQ4wDAYDVQQH
DAVQYXJpczEMMAoGA1UECgwDT0NBMQwwCgYDVQQLDANPQ0ExFDASBgNVBAMMC2V4
YW1wbGUuY29tMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTIz
MDEwMTExMDAyN1oXDTIzMDEzMTExMDAyN1owgYUxCzAJBgNVBAYTAkFVMRMwEQYD
VQQIDApTb21lLVN0YXRlMQ4wDAYDVQQHDAVQYXJpczEMMAoGA1UECgwDT0NBMQww
CgYDVQQLDANPQ0ExFDASBgNVBAMMC2V4YW1wbGUuY29tMR8wHQYJKoZIhvcNAQkB
FhB0ZXN0QGV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAvgeLRr1Q9aS/t8ZuC7/pZRHTB6sqamVwXyR7zh0v51yH7xBy9xs4zJWKneRn
OJw46IogYhY+dyNWElbY+Ckcc6z1eJONiHNtOKAy07VtfhisGviRv1kLE56SHGgW
fIXrOuFqj6F1yTfKyLtq2RZBzmbMTNG7z89rO2hqdTWqhyof9OGWtecrM7Ei9PnL
tqULhQyh6n47KnIXfBMLIeQG7d/WyGU5CnO7yhHkS/51E9gI6g5G0VoueBVFErCl
rjo0clMJrFVpanOG2USGgLfPkomSIv9ZL4SreFN27sbhTbkVWxbk7AOCFCQcaBIv
RThpRrA9YRv2dB/X4yIi7UrrPwIDAQABo1MwUTAdBgNVHQ4EFgQU4WFoM/SL6qvT
jV4YUwH3rggBqyIwHwYDVR0jBBgwFoAU4WFoM/SL6qvTjV4YUwH3rggBqyIwDwYD
VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYG+CTENdcmiKmyYg/n6H
59RRgaLHSszjsKYdM5Uy/PmwfvPTRuxP38UhF0gWJTsLxWRjI0ejzdy5vzvxpoYl
3pEyYxPzl+jfGA6AUSOfzTT/ksi7SZWak3cqJDFCdnfaCSjYyi+nKz5y6Bm1/fMy
/3zJX92ELIWc8cTUItUIWjlITmxgLhIGr1wYaCinxkaEopGqkX85RYFaWKyYa5ok
8MnoYbPrh/i9EekHUMBMPKWN3tWMMEROTtX9hmxSSTtgdQCahBaOCCU+m8PSNKEc
UA8nSStaolv8t6aOyEb/Kzs7WSbd7v1ovZsy2FYmIRn0eHz8fpMAw2qk7mol6iao
GQ==
-----END CERTIFICATE-----

View file

@ -0,0 +1,49 @@
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAOOSWfQt/sCwMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMjEwMzAyMjE0ODMwWhcNMjEwNDAxMjE0ODMwWjBF
MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAtBIWiINhfAGdNHiNoG9xzz/PPQvgPKKz6RirHpcx7RAaCUcnsTcwZILH
8aQ+lUbP62lIhCQ6o7/EuJt7AGxaOclM6E117o4eDQTcNPekhlqcUpFmL21V2dZg
fL0tfwtu6ny6h/pJsgy66LF1YPgUrHA6e4TFWZOIXL5KHZivuveyeyu+hb14CwzB
uvRMPgI7GeyxrhXlBsfntxKnD9cz4brnM2Eznuy8jSufvh3urCLphek9z4Bnfdsa
7I7bCTAHjkUegWWXiAcRgd+Uluhcu1xQB1h4135dehVKnoaBTgFVUl8IDsqZN1yg
/s7JzCA4iI23JEI0lHIJHTQ2GhpxtQIDAQABo1AwTjAdBgNVHQ4EFgQUXXIizCA8
7eJpzk2fI2od4Inq4CQwHwYDVR0jBBgwFoAUXXIizCA87eJpzk2fI2od4Inq4CQw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAIohOqEpKjkLBySQYTOdO
ZkO31B3YnVXX9o2rN8ulVzfQNKcvKtGiME8ubcmrFg49sP+T3Y14hG4KfYFJ6zcC
qveitFcmQ8NXHgByTqPGi9ZDdwEBKeHCHIym0R+24hEHw59iH42f6IVtfOZOR02P
UyH1AKY2rktXNatOLysNYigf3JN6WlwahEaQ4XOrx+l0ez8H0mqR51rRUhdxuyBJ
RPhqpWqrWhm6Hmcgv17/PwEQRHsFBF/qWb6iT9Cv0IR8v5gnlimaYyF1gacHKbkb
gpit88248SSh1joHnhyoM+yTVnZiL5xAcuf5HicOmPOMLaKv6pJ/pqNz9jy0V3/U
iw==
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0EhaIg2F8AZ00
eI2gb3HPP889C+A8orPpGKselzHtEBoJRyexNzBkgsfxpD6VRs/raUiEJDqjv8S4
m3sAbFo5yUzoTXXujh4NBNw096SGWpxSkWYvbVXZ1mB8vS1/C27qfLqH+kmyDLro
sXVg+BSscDp7hMVZk4hcvkodmK+697J7K76FvXgLDMG69Ew+AjsZ7LGuFeUGx+e3
EqcP1zPhuuczYTOe7LyNK5++He6sIumF6T3PgGd92xrsjtsJMAeORR6BZZeIBxGB
35SW6Fy7XFAHWHjXfl16FUqehoFOAVVSXwgOypk3XKD+zsnMIDiIjbckQjSUcgkd
NDYaGnG1AgMBAAECggEAFpPCAYG/gkXNiRuoXjo64cpVWIkZp2CbABnYsrAwUVHY
gdtLDbwmtCN1oEWAl0TWouSDdBX6yDcuGhtcc7QiJ+amXuX/aFanS+iVF4sJNNM9
kFisoDusLPDlDh7GCozLblkPJidqgAl6kdxWJD9WkDxOCNifydhmm4I8VrOjLOTV
w5gF0kjSRJmkNbPXiSOmaZzJEuxJYR9tqpanf2Fh/6Xf50Y5Hd+25mqbKUg21lZy
AmHk5DYFZMugPFZgPzcZayr0LotlmfiExrMtrKoI2UX9J//MGrVil1bp8iVTJElJ
fDPAivpcTjO5AlHRl2dfLM4ZUub6E4bw+DDGUX04DQKBgQDYRd0jp8S44763+6sA
/F8jKNzxn5nCOkFUxMalh9+wzd433MsXj50QTVy3BGbm+mmo8ybqcQ2PVDI9mo3n
73g4eG/BeSeYSDfk1gqIP5pb9gaAYKmKrNFphCqvrbJsCwzQ+bgrd3WKAv/isIlT
756B/cl+RP6doDcTsFcBV9VjHwKBgQDVJdRnBAXgeeY8XP2sbd8Mr8Ev8Wf4IpTh
dNN3A+ekcYyBkSdXeB8j+ToAcBWM5T69gofG61V6RHvAUPTf3AU9hWs1K2Xm5+Ln
SEstDwDQ+DdcASCkgC48j2anvlla17Tf5Tcl6DaksxGHYNhT1U+hCiaDDU03mrDF
aVIFbSNEqwKBgDZ0GMLiefindxyx5BOCd53Nqxu3OKqbqlliljWVaXAF1Z6xG/2Z
rk0tfVujYxljEXl1h2XeAzEEXQX/xR0RwW5OfKz1CVAhVtlqPwqhIQdogaiPLgD5
lFyB55GGJXdorNhtF77x/Ak8yhrUoi8dFQbb1IDTdFxRu6xcaPuwlsy3AoGBAKdG
5hfmz1npMOiErkzpeVhygnHGyiqxsRfzYJYRyXSD7Jouuapqyj2oNX3seO03aHLA
AyD4xf+LyXcX0eXxvWcX0xhKM9HwgGG0mdMF6EUX2BJrjButwRukCxNwTp39laT1
Nb+ZK3E8W3Bcb8nzKWggGDNXeBdAXqS/UDCUA067AoGAUhJg9//JkziNYImCjInX
nsYOH1ojfxtm5pr9+yiobKGdtUe36MQZmW6JMfiqJVjw5NYBgykbDXwrOkK97IeR
2xtx0jcNLyadhzoWIHU/OvJRC1tsdOV30PdsiIRYbpuZjoBiDd2wODeylm9WG9Xz
N6TBKKvJflx4Sw2o7+4EfZU=
-----END PRIVATE KEY-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+B4tGvVD1pL+3
xm4Lv+llEdMHqypqZXBfJHvOHS/nXIfvEHL3GzjMlYqd5Gc4nDjoiiBiFj53I1YS
Vtj4KRxzrPV4k42Ic204oDLTtW1+GKwa+JG/WQsTnpIcaBZ8hes64WqPoXXJN8rI
u2rZFkHOZsxM0bvPz2s7aGp1NaqHKh/04Za15yszsSL0+cu2pQuFDKHqfjsqchd8
Ewsh5Abt39bIZTkKc7vKEeRL/nUT2AjqDkbRWi54FUUSsKWuOjRyUwmsVWlqc4bZ
RIaAt8+SiZIi/1kvhKt4U3buxuFNuRVbFuTsA4IUJBxoEi9FOGlGsD1hG/Z0H9fj
IiLtSus/AgMBAAECggEBAKuXUFJeHL7TNzMRAMmnT28uOypPiwtr8Z5X6Vtiy6DU
0wIyDj3H3PAPkI2mcvaRSmngYAFyKJGX3N7OgTkElmZ1pWptgn3WDKf3MC4vQ2F7
kd0A20q3cuMSaskvzC5BFvmiFoD/wMYjlP7RDVhdWqqv9IbhVAAAQcnxLUANZ6CH
/xrieGuYavs62pSu5fnke7zRozdD1Mb7/oolAnycaLuoi1eZBh8wW8EJyFSxcZ5A
pYF5kNqbwAdOZ22Tygxwu7lnh8PUOKxf9pTmO6uUYAJcn/Z3ZHtnBYsjU/LkfNPV
hYLu1bKftm6UEZYwCXE3/ygop1q648NvCvtJB+Gbj9ECgYEA8nB+hS+7MLgi/dv8
FCMJ9HBN76/nlwjOCTZIyIhCs5Jc6zJQGiDNLUFM/1mpBKUWWAss3g0dmJq32ish
apsCUxabzWuKi44fDMEterJrGDWquyJK+jNPqfqOORLdMf0edNfZbjUxev7D52Ak
4Ej3Ggy/fENd8QWLK6PZHV5X1MUCgYEAyKiWlawh7l8eBrba8UFQ4n1HiK/2uEud
yQOLceSRmW/xC6ZCiR0ILinrtZWRxqQg+ZSS24hjnHhcdnRw8TRXx22TkTwGfAXW
wKesPrtGJrn0ADuZwPkGewyeHPsisXNSiuGLPcLiOCoNNYgbIWJ2RknM1Xw+2p8C
qYU8Si6l6DMCgYEA20v4ld7sExCszjZ72XcsXQhs5v+Vm9/iByEsSwA+XZJqLHFx
VYEQNvxXeq8OnN37zR4msqDogY6J+XWEH5shSiksO28ofj3LRk1DJzZWeyqoSeem
LJXXXKkAlw3COaJ9NzG8Qt0o6dmjORqVoK8/nTekyfFh+0+JaKsoDFG3XwUCgYBN
tq2Ljj0d+wzAAPXO1kMjVO3tjGj7e53CinLpS2LwkCBFKMFAJVRTvLyjeSgaTNrQ
jrBKAgrCQQNehT5wzJrqjA/JAfxo8EH6H3ZgXVuQCBjuNicYS9ossfhStRj8rPNd
AnlRFDdVFUREZVBMn7u7AT4puJMHTOpVCVsOR/7NbQKBgApyR1WfsxZYi8vzVosQ
jnMIW18lnZN3s6auyEvmpVowx0U0yd9QU3HHX1j8Kfq7D9uERCwBtIfn9fzZrZnu
Xgbi9LMUT1z1jFXxJNgzaNmm0JHW9cD24BWNeQ60uxaRiGGmCyfmgqrGOXSn2R8w
KoWEnnunZ9nehcD9dkWcH5zG
-----END PRIVATE KEY-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0EhaIg2F8AZ00
eI2gb3HPP889C+A8orPpGKselzHtEBoJRyexNzBkgsfxpD6VRs/raUiEJDqjv8S4
m3sAbFo5yUzoTXXujh4NBNw096SGWpxSkWYvbVXZ1mB8vS1/C27qfLqH+kmyDLro
sXVg+BSscDp7hMVZk4hcvkodmK+697J7K76FvXgLDMG69Ew+AjsZ7LGuFeUGx+e3
EqcP1zPhuuczYTOe7LyNK5++He6sIumF6T3PgGd92xrsjtsJMAeORR6BZZeIBxGB
35SW6Fy7XFAHWHjXfl16FUqehoFOAVVSXwgOypk3XKD+zsnMIDiIjbckQjSUcgkd
NDYaGnG1AgMBAAECggEAFpPCAYG/gkXNiRuoXjo64cpVWIkZp2CbABnYsrAwUVHY
gdtLDbwmtCN1oEWAl0TWouSDdBX6yDcuGhtcc7QiJ+amXuX/aFanS+iVF4sJNNM9
kFisoDusLPDlDh7GCozLblkPJidqgAl6kdxWJD9WkDxOCNifydhmm4I8VrOjLOTV
w5gF0kjSRJmkNbPXiSOmaZzJEuxJYR9tqpanf2Fh/6Xf50Y5Hd+25mqbKUg21lZy
AmHk5DYFZMugPFZgPzcZayr0LotlmfiExrMtrKoI2UX9J//MGrVil1bp8iVTJElJ
fDPAivpcTjO5AlHRl2dfLM4ZUub6E4bw+DDGUX04DQKBgQDYRd0jp8S44763+6sA
/F8jKNzxn5nCOkFUxMalh9+wzd433MsXj50QTVy3BGbm+mmo8ybqcQ2PVDI9mo3n
73g4eG/BeSeYSDfk1gqIP5pb9gaAYKmKrNFphCqvrbJsCwzQ+bgrd3WKAv/isIlT
756B/cl+RP6doDcTsFcBV9VjHwKBgQDVJdRnBAXgeeY8XP2sbd8Mr8Ev8Wf4IpTh
dNN3A+ekcYyBkSdXeB8j+ToAcBWM5T69gofG61V6RHvAUPTf3AU9hWs1K2Xm5+Ln
SEstDwDQ+DdcASCkgC48j2anvlla17Tf5Tcl6DaksxGHYNhT1U+hCiaDDU03mrDF
aVIFbSNEqwKBgDZ0GMLiefindxyx5BOCd53Nqxu3OKqbqlliljWVaXAF1Z6xG/2Z
rk0tfVujYxljEXl1h2XeAzEEXQX/xR0RwW5OfKz1CVAhVtlqPwqhIQdogaiPLgD5
lFyB55GGJXdorNhtF77x/Ak8yhrUoi8dFQbb1IDTdFxRu6xcaPuwlsy3AoGBAKdG
5hfmz1npMOiErkzpeVhygnHGyiqxsRfzYJYRyXSD7Jouuapqyj2oNX3seO03aHLA
AyD4xf+LyXcX0eXxvWcX0xhKM9HwgGG0mdMF6EUX2BJrjButwRukCxNwTp39laT1
Nb+ZK3E8W3Bcb8nzKWggGDNXeBdAXqS/UDCUA067AoGAUhJg9//JkziNYImCjInX
nsYOH1ojfxtm5pr9+yiobKGdtUe36MQZmW6JMfiqJVjw5NYBgykbDXwrOkK97IeR
2xtx0jcNLyadhzoWIHU/OvJRC1tsdOV30PdsiIRYbpuZjoBiDd2wODeylm9WG9Xz
N6TBKKvJflx4Sw2o7+4EfZU=
-----END PRIVATE KEY-----

View file

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAOOSWfQt/sCwMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMjEwMzAyMjE0ODMwWhcNMjEwNDAxMjE0ODMwWjBF
MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAtBIWiINhfAGdNHiNoG9xzz/PPQvgPKKz6RirHpcx7RAaCUcnsTcwZILH
8aQ+lUbP62lIhCQ6o7/EuJt7AGxaOclM6E117o4eDQTcNPekhlqcUpFmL21V2dZg
fL0tfwtu6ny6h/pJsgy66LF1YPgUrHA6e4TFWZOIXL5KHZivuveyeyu+hb14CwzB
uvRMPgI7GeyxrhXlBsfntxKnD9cz4brnM2Eznuy8jSufvh3urCLphek9z4Bnfdsa
7I7bCTAHjkUegWWXiAcRgd+Uluhcu1xQB1h4135dehVKnoaBTgFVUl8IDsqZN1yg
/s7JzCA4iI23JEI0lHIJHTQ2GhpxtQIDAQABo1AwTjAdBgNVHQ4EFgQUXXIizCA8
7eJpzk2fI2od4Inq4CQwHwYDVR0jBBgwFoAUXXIizCA87eJpzk2fI2od4Inq4CQw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAIohOqEpKjkLBySQYTOdO
ZkO31B3YnVXX9o2rN8ulVzfQNKcvKtGiME8ubcmrFg49sP+T3Y14hG4KfYFJ6zcC
qveitFcmQ8NXHgByTqPGi9ZDdwEBKeHCHIym0R+24hEHw59iH42f6IVtfOZOR02P
UyH1AKY2rktXNatOLysNYigf3JN6WlwahEaQ4XOrx+l0ez8H0mqR51rRUhdxuyBJ
RPhqpWqrWhm6Hmcgv17/PwEQRHsFBF/qWb6iT9Cv0IR8v5gnlimaYyF1gacHKbkb
gpit88248SSh1joHnhyoM+yTVnZiL5xAcuf5HicOmPOMLaKv6pJ/pqNz9jy0V3/U
iw==
-----END CERTIFICATE-----

View file

@ -0,0 +1,177 @@
import os
from urllib.parse import parse_qs, urlparse
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT, pack
from saml2.authn_context import INTERNETPROTOCOLPASSWORD
from saml2.config import Config as Saml2Config
from saml2.metadata import create_metadata_string
from saml2.saml import NAME_FORMAT_URI, NAMEID_FORMAT_PERSISTENT
from saml2.server import Server
TYP = {"GET": [BINDING_HTTP_REDIRECT], "POST": [BINDING_HTTP_POST]}
AUTHN = {
"class_ref": INTERNETPROTOCOLPASSWORD,
"authn_auth": "http://www.example.com/login",
}
BASE = "http://localhost:8000"
CONFIG = {
"entityid": "urn:mace:example.com:saml:example:idp",
"name": "Rolands IdP",
"service": {
"aa": {
"endpoints": {
"attribute_service": [
("%s/aap" % BASE, BINDING_HTTP_POST),
]
},
},
"aq": {
"endpoints": {
"authn_query_service": [("%s/aqs" % BASE, BINDING_HTTP_POST)]
},
},
"idp": {
"endpoints": {
"single_sign_on_service": [
("%s/sso/redirect" % BASE, BINDING_HTTP_REDIRECT),
("%s/sso/post" % BASE, BINDING_HTTP_POST),
],
},
"policy": {
"default": {
"lifetime": {"minutes": 15},
"attribute_restrictions": None,
"name_form": NAME_FORMAT_URI,
},
"urn:mace:example.com:saml:example:sp": {
"lifetime": {"minutes": 5},
"nameid_format": NAMEID_FORMAT_PERSISTENT,
},
},
},
},
"debug": 1,
"key_file": os.path.join(os.path.dirname(__file__), "data", "idp.pem"),
"cert_file": os.path.join(os.path.dirname(__file__), "data", "idp.pem"),
"organization": {
"name": "Example",
"display_name": [("Example", "uk")],
"url": "http://www.example.com/",
},
"contact_person": [
{
"given_name": "Admin",
"sur_name": "Admin",
"email_address": ["admin@example.com"],
"contact_type": "technical",
},
],
}
class DummyResponse:
def __init__(self, status, data, headers=None):
self.status_code = status
self.text = data
self.headers = headers or []
self.content = data
def _unpack(self, ver="SAMLResponse"):
"""
Unpack the response form
"""
_str = self.text
sr_str = 'name="%s" value="' % ver
rs_str = 'name="RelayState" value="'
i = _str.find(sr_str)
i += len(sr_str)
j = _str.find('"', i)
sr = _str[i:j]
start = _str.find(rs_str, j)
start += len(rs_str)
end = _str.find('"', start)
rs = _str[start:end]
return {ver: sr, "RelayState": rs}
class FakeIDP(Server):
def __init__(self, metadatas=None, settings=None):
if settings is None:
settings = CONFIG
if metadatas:
settings.update({"metadata": {"inline": metadatas}})
config = Saml2Config()
config.load(settings)
config.allow_unknown_attributes = True
Server.__init__(self, config=config)
def get_metadata(self):
return create_metadata_string(
None,
config=self.config,
sign=True,
valid=True,
cert=CONFIG.get("cert_file"),
keyfile=CONFIG.get("key_file"),
)
def fake_login(self, url):
# Assumes GET query and HTTP_REDIRECT only.
# This is all that auth_pysaml currently supports.
parsed_url = urlparse(url)
qs_dict = parse_qs(parsed_url.query)
samlreq = qs_dict["SAMLRequest"][0]
rstate = qs_dict["RelayState"][0]
# process the logon request, and automatically "login"
return self.authn_request_endpoint(samlreq, BINDING_HTTP_REDIRECT, rstate)
def authn_request_endpoint(self, req, binding, relay_state):
req = self.parse_authn_request(req, binding)
if req.message.protocol_binding == BINDING_HTTP_REDIRECT:
_binding = BINDING_HTTP_POST
else:
_binding = req.message.protocol_binding
resp_args = self.response_args(req.message, [_binding])
identity = {
"surName": "Example",
"givenName": "Test",
"title": "Ind",
"mail": "test@example.com",
}
resp_args.update({"sign_assertion": True, "sign_response": True})
authn_resp = self.create_authn_response(
identity, userid=identity.get("mail"), authn=AUTHN, **resp_args
)
_dict = pack.factory(
_binding, authn_resp, resp_args["destination"], relay_state, "SAMLResponse"
)
return DummyResponse(**_dict)
class UnsignedFakeIDP(FakeIDP):
def create_authn_response(
self,
*args,
**kwargs,
):
kwargs["sign_assertion"] = False
return super().create_authn_response(*args, **kwargs)

View file

@ -0,0 +1,488 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import base64
import html
import os
import os.path as osp
from copy import deepcopy
from unittest.mock import patch
import responses
from saml2.sigver import SignatureError
from odoo.exceptions import AccessDenied, UserError, ValidationError
from odoo.tests import HttpCase, tagged
from odoo.tools import mute_logger
from .fake_idp import CONFIG, FakeIDP, UnsignedFakeIDP
@tagged("saml", "post_install", "-at_install")
class TestPySaml(HttpCase):
def setUp(self):
super().setUp()
sp_pem_public = None
sp_pem_private = None
with open(
os.path.join(os.path.dirname(__file__), "data", "sp.pem"),
"r",
encoding="UTF-8",
) as file:
sp_pem_public = file.read()
with open(
os.path.join(os.path.dirname(__file__), "data", "sp.key"),
"r",
encoding="UTF-8",
) as file:
sp_pem_private = file.read()
self.saml_provider = self.env["auth.saml.provider"].create(
{
"name": "SAML Provider Demo",
"idp_metadata": FakeIDP().get_metadata(),
"sp_pem_public": base64.b64encode(sp_pem_public.encode()),
"sp_pem_private": base64.b64encode(sp_pem_private.encode()),
"body": "Login with Authentic",
"active": True,
"sig_alg": "SIG_RSA_SHA1",
"matching_attribute": "mail",
}
)
self.url_saml_request = (
"/auth_saml/get_auth_request?pid=%d" % self.saml_provider.id
)
self.idp = FakeIDP([self.saml_provider._metadata_string()])
# Create a user with only password, and another with both password and saml id
self.user, self.user2 = (
self.env["res.users"]
.with_context(no_reset_password=True, tracking_disable=True)
.create(
[
{
"name": "User",
"email": "test@example.com",
"login": "test@example.com",
"password": "Lu,ums-7vRU>0i]=YDLa",
},
{
"name": "User with SAML",
"email": "user@example.com",
"login": "user@example.com",
"password": "NesTNSte9340D720te>/-A",
"saml_ids": [
(
0,
0,
{
"saml_provider_id": self.saml_provider.id,
"saml_uid": "user@example.com",
},
)
],
},
]
)
)
def test_ensure_provider_appears_on_login(self):
# SAML provider should be listed in the login page
response = self.url_open("/web/login")
self.assertIn("Login with Authentic", response.text)
self.assertIn(self.url_saml_request, response.text)
def test_ensure_provider_appears_on_login_with_redirect_param(self):
"""Test that SAML provider is listed in the login page keeping the redirect"""
response = self.url_open(
"/web/login?redirect=%2Fweb%23action%3D37%26model%3Dir.module.module%26view"
"_type%3Dkanban%26menu_id%3D5"
)
self.assertIn("Login with Authentic", response.text)
self.assertIn(
"/auth_saml/get_auth_request?pid={}&redirect=%2Fweb%23action%3D37%26mod"
"el%3Dir.module.module%26view_type%3Dkanban%26menu_id%3D5".format(
self.saml_provider.id
),
response.text,
)
def test_ensure_metadata_present(self):
response = self.url_open(
"/auth_saml/metadata?p=%d&d=%s"
% (self.saml_provider.id, self.env.cr.dbname)
)
self.assertTrue(response.ok)
self.assertTrue("xml" in response.headers.get("Content-Type"))
def test_ensure_get_auth_request_redirects(self):
response = self.url_open(
"/auth_saml/get_auth_request?pid=%d" % self.saml_provider.id,
allow_redirects=False,
)
self.assertTrue(response.ok)
self.assertEqual(response.status_code, 303)
self.assertIn(
"http://localhost:8000/sso/redirect?SAMLRequest=",
response.headers.get("Location"),
)
def test_login_no_saml(self):
"""
Login with a user account, but without any SAML provider setup
against the user
"""
# Standard login using password
self.authenticate(user="test@example.com", password="Lu,ums-7vRU>0i]=YDLa")
self.assertEqual(self.session.uid, self.user.id)
self.logout()
# Try to log in with a non-existing SAML token
with self.assertRaises(AccessDenied):
self.authenticate(user="test@example.com", password="test_saml_token")
redirect_url = self.saml_provider._get_auth_request()
self.assertIn("http://localhost:8000/sso/redirect?SAMLRequest=", redirect_url)
response = self.idp.fake_login(redirect_url)
self.assertEqual(200, response.status_code)
unpacked_response = response._unpack()
with self.assertRaises(AccessDenied):
self.env["res.users"].sudo().auth_saml(
self.saml_provider.id, unpacked_response.get("SAMLResponse"), None
)
def add_provider_to_user(self):
"""Add a provider to self.user"""
self.user.write(
{
"saml_ids": [
(
0,
0,
{
"saml_provider_id": self.saml_provider.id,
"saml_uid": "test@example.com",
},
)
]
}
)
def test_login_with_saml(self):
self.add_provider_to_user()
redirect_url = self.saml_provider._get_auth_request()
self.assertIn("http://localhost:8000/sso/redirect?SAMLRequest=", redirect_url)
response = self.idp.fake_login(redirect_url)
self.assertEqual(200, response.status_code)
unpacked_response = response._unpack()
(database, login, token) = (
self.env["res.users"]
.sudo()
.auth_saml(
self.saml_provider.id, unpacked_response.get("SAMLResponse"), None
)
)
self.assertEqual(database, self.env.cr.dbname)
self.assertEqual(login, self.user.login)
# We should not be able to log in with the wrong token
with self.assertRaises(AccessDenied):
self.authenticate(
user="test@example.com", password="{}-WRONG".format(token)
)
# User should now be able to log in with the token
self.authenticate(user="test@example.com", password=token)
def test_disallow_user_password_when_changing_ir_config_parameter(self):
"""Test that disabling users from having both a password and SAML ids remove
users password."""
# change the option
self.browse_ref(
"auth_saml.allow_saml_uid_and_internal_password"
).value = "False"
# The password should be blank and the user should not be able to connect
with self.assertRaises(AccessDenied):
self.authenticate(
user="user@example.com", password="NesTNSte9340D720te>/-A"
)
def test_disallow_user_password_new_user(self):
"""Test that a new user can not be set up with both password and SAML ids when
the disallow option is set."""
# change the option
self.browse_ref(
"auth_saml.allow_saml_uid_and_internal_password"
).value = "False"
with self.assertRaises(UserError):
self.env["res.users"].with_context(no_reset_password=True).create(
{
"name": "New user with SAML",
"email": "user2@example.com",
"login": "user2@example.com",
"password": "NesTNSte9340D720te>/-A",
"saml_ids": [
(
0,
0,
{
"saml_provider_id": self.saml_provider.id,
"saml_uid": "user2",
},
)
],
}
)
def test_disallow_user_password_no_password_set(self):
"""Test that a new user with SAML ids can not have its password set up when the
disallow option is set."""
# change the option
self.browse_ref(
"auth_saml.allow_saml_uid_and_internal_password"
).value = "False"
# Create a new user with only SAML ids
user = (
self.env["res.users"]
.with_context(no_reset_password=True, tracking_disable=True)
.create(
{
"name": "New user with SAML",
"email": "user2@example.com",
"login": "user2@example.com",
"saml_ids": [
(
0,
0,
{
"saml_provider_id": self.saml_provider.id,
"saml_uid": "unused",
},
)
],
}
)
)
# Assert that the user password can not be set
with self.assertRaises(ValidationError):
user.password = "new password"
def test_disallow_user_password(self):
"""Test that existing user password is deleted when adding an SAML provider when
the disallow option is set."""
# change the option
self.browse_ref(
"auth_saml.allow_saml_uid_and_internal_password"
).value = "False"
# Test that existing user password is deleted when adding an SAML provider
self.authenticate(user="test@example.com", password="Lu,ums-7vRU>0i]=YDLa")
self.add_provider_to_user()
with self.assertRaises(AccessDenied):
self.authenticate(user="test@example.com", password="Lu,ums-7vRU>0i]=YDLa")
def test_disallow_user_admin_can_have_password(self):
"""Test that admin can have its password set even if the disallow option is set."""
# change the option
self.browse_ref(
"auth_saml.allow_saml_uid_and_internal_password"
).value = "False"
# Test base.user_admin exception
self.env.ref("base.user_admin").password = "nNRST4j*->sEatNGg._!"
def test_db_filtering(self):
# change filter to only allow our db.
with patch("odoo.http.db_filter", new=lambda *args, **kwargs: []):
self.add_provider_to_user()
redirect_url = self.saml_provider._get_auth_request()
response = self.idp.fake_login(redirect_url)
unpacked_response = response._unpack()
for key in unpacked_response:
unpacked_response[key] = html.unescape(unpacked_response[key])
response = self.url_open("/auth_saml/signin", data=unpacked_response)
self.assertFalse(response.ok)
self.assertIn(response.status_code, [400, 404])
def test_redirect_after_login(self):
"""Test that providing a redirect will be kept after SAML login."""
self.add_provider_to_user()
redirect_url = self.saml_provider._get_auth_request(
{
"r": "%2Fweb%23action%3D37%26model%3Dir.module.module%26view_type%3Dkan"
"ban%26menu_id%3D5"
}
)
response = self.idp.fake_login(redirect_url)
unpacked_response = response._unpack()
for key in unpacked_response:
unpacked_response[key] = html.unescape(unpacked_response[key])
response = self.url_open(
"/auth_saml/signin",
data=unpacked_response,
allow_redirects=True,
timeout=300,
)
self.assertTrue(response.ok)
self.assertEqual(
response.url,
self.base_url()
+ "/web#action=37&model=ir.module.module&view_type=kanban&menu_id=5",
)
def test_disallow_user_password_when_changing_settings(self):
"""Test that disabling the setting will remove passwords from related users"""
# We activate the settings to allow password login
self.env["res.config.settings"].create(
{
"allow_saml_uid_and_internal_password": True,
}
).execute()
# Test the user can login with the password
self.authenticate(user="user@example.com", password="NesTNSte9340D720te>/-A")
self.env["res.config.settings"].create(
{
"allow_saml_uid_and_internal_password": False,
}
).execute()
with self.assertRaises(AccessDenied):
self.authenticate(
user="user@example.com", password="NesTNSte9340D720te>/-A"
)
@responses.activate
def test_download_metadata(self):
expected_metadata = self.idp.get_metadata()
responses.add(
responses.GET,
"http://localhost:8000/metadata",
status=200,
content_type="text/xml",
body=expected_metadata,
)
self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata"
self.saml_provider.idp_metadata = ""
self.saml_provider.action_refresh_metadata_from_url()
self.assertEqual(self.saml_provider.idp_metadata, expected_metadata)
@responses.activate
def test_download_metadata_no_provider(self):
self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata"
self.saml_provider.idp_metadata = ""
self.saml_provider.active = False
self.saml_provider.action_refresh_metadata_from_url()
self.assertFalse(self.saml_provider.idp_metadata)
@responses.activate
def test_download_metadata_error(self):
responses.add(
responses.GET,
"http://localhost:8000/metadata",
status=500,
content_type="text/xml",
)
self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata"
self.saml_provider.idp_metadata = ""
with self.assertRaises(UserError):
self.saml_provider.action_refresh_metadata_from_url()
self.assertFalse(self.saml_provider.idp_metadata)
@responses.activate
def test_download_metadata_no_update(self):
expected_metadata = self.idp.get_metadata()
responses.add(
responses.GET,
"http://localhost:8000/metadata",
status=200,
content_type="text/xml",
body=expected_metadata,
)
self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata"
self.saml_provider.idp_metadata = expected_metadata
self.saml_provider.action_refresh_metadata_from_url()
self.assertEqual(self.saml_provider.idp_metadata, expected_metadata)
@responses.activate
def test_login_with_saml_metadata_empty(self):
self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata"
self.saml_provider.idp_metadata = ""
expected_metadata = self.idp.get_metadata()
responses.add(
responses.GET,
"http://localhost:8000/metadata",
status=200,
content_type="text/xml",
body=expected_metadata,
)
self.test_login_with_saml()
self.assertEqual(self.saml_provider.idp_metadata, expected_metadata)
@responses.activate
def test_login_with_saml_metadata_key_changed(self):
settings = deepcopy(CONFIG)
settings["key_file"] = osp.join(
osp.dirname(__file__), "data", "key_idp_expired.pem"
)
settings["cert"] = osp.join(
osp.dirname(__file__), "data", "key_idp_expired.pem"
)
expired_idp = FakeIDP(settings=settings)
self.saml_provider.idp_metadata = expired_idp.get_metadata()
self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata"
up_to_date_metadata = self.idp.get_metadata()
self.assertNotEqual(self.saml_provider.idp_metadata, up_to_date_metadata)
responses.add(
responses.GET,
"http://localhost:8000/metadata",
status=200,
content_type="text/xml",
body=up_to_date_metadata,
)
self.test_login_with_saml()
@responses.activate
def test_login_with_saml_unsigned_response(self):
self.add_provider_to_user()
self.saml_provider.idp_metadata_url = "http://localhost:8000/metadata"
unsigned_idp = UnsignedFakeIDP([self.saml_provider._metadata_string()])
redirect_url = self.saml_provider._get_auth_request()
self.assertIn("http://localhost:8000/sso/redirect?SAMLRequest=", redirect_url)
response = unsigned_idp.fake_login(redirect_url)
self.assertEqual(200, response.status_code)
unpacked_response = response._unpack()
responses.add(
responses.GET,
"http://localhost:8000/metadata",
status=200,
content_type="text/xml",
body=self.saml_provider.idp_metadata,
)
with (
self.assertRaises(SignatureError),
mute_logger("saml2.entity"),
mute_logger("saml2.client_base"),
):
(database, login, token) = (
self.env["res.users"]
.sudo()
.auth_saml(
self.saml_provider.id, unpacked_response.get("SAMLResponse"), None
)
)