99
1010
1111logger = logging .getLogger (__name__ )
12- logging .basicConfig (level = logging .DEBUG )
12+ logging .basicConfig (level = logging .INFO )
1313
14- def get_lab_user (query ):
15- # Based on https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/LAB.aspx
16- user = requests .get ("https://api.msidlab.com/api/user" , params = query ).json ()
17- return { # Mapping lab API response to our expected configuration format
18- "authority" : user ["Authority" ][0 ] + user ["Users" ]["tenantId" ],
19- "client_id" : user ["AppID" ],
20- "username" : user ["Users" ]["upn" ],
21- "password" : "TBD" , # TODO
22- "scope" : ["https://graph.microsoft.com/.default" ],
23- }
24-
25- def get_lab_app (
26- env_client_id = "LAB_APP_CLIENT_ID" ,
27- env_client_secret = "LAB_APP_CLIENT_SECRET" ,
28- ):
29- """Returns the lab app as an MSAL confidential client.
3014
31- Get it from environment variables if defined, otherwise fall back to use MSI.
32- """
33- if os .getenv (env_client_id ) and os .getenv (env_client_secret ):
34- # A shortcut mainly for running tests on developer's local development machine
35- # or it could be setup on Travis CI
36- # https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-settings
37- # Data came from here
38- # https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Rese.aspx#programmatic-access-info-for-lab-request-api
39- logger .info ("Using lap app defined by ENV variables %s and %s" ,
40- env_client_id , env_client_secret )
41- client_id = os .getenv (env_client_id )
42- client_secret = os .getenv (env_client_secret )
43- else :
44- logger .info ("ENV variables %s and/or %s are not defined. Fall back to MSI." ,
45- env_client_id , env_client_secret )
46- # See also https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Programmatically-accessing-LAB-API's.aspx
47- raise NotImplementedError ("MSI-based mechanism has not been implemented yet" )
48- return msal .ConfidentialClientApplication (client_id , client_secret ,
49- authority = "https://login.microsoftonline.com/"
50- "72f988bf-86f1-41af-91ab-2d7cd011db47" , # Microsoft tenant ID
51- )
52-
53- def get_lab_user_secret (access_token , lab_name = "msidlab4" ):
54- return requests .get (
55- # Note: Short link won't work "https://aka.ms/GetLabUserSecret?Secret=%s"
56- "https://request.msidlab.com/api/GetLabUserSecret?code=KpY5uCcoKo0aW8VOL/CUO3wnu9UF2XbSnLFGk56BDnmQiwD80MQ7HA==&Secret=%s"
57- % lab_name ,
58- headers = {"Authorization" : "Bearer %s" % access_token },
59- ).json ()["Value" ]
60-
61-
62- @unittest .skip ("for now" )
6315class E2eTestCase (unittest .TestCase ):
64- """
65- lab_token = get_lab_app().acquire_token_for_client(
66- "https://request.msidlab.com/.default"
67- ) # BTW, this infrastructure tests the confidential client flow
68- lab_password = get_lab_user_secret(lab_token["access_token"])
69- """
70-
71- def setUp (self ):
72- pass
73- # client_id, client_secret = get_lab_app()
74- # self.lab_app = msal.ConfidentialClientApplication(client_id, client_secret)
75-
76- def test_bar (self ):
77- self .assertEqual ("********" , self .lab_password )
78-
79-
80- class BaseMixin (object ):
16+ config = {}
8117
8218 def skipIfNotConfigured (self , fields ):
83- if not all (map (self .config .get , fields )):
84- self .skipTest ("Configuration not sufficient" )
8519 for field in fields :
8620 if not self .config .get (field ):
87- self .skipTest ('"%s" not defined in configuration' % field )
21+ self .skipTest ('"%s" not found in configuration' % field )
8822
8923 def assertLoosely (self , response , assertion = None ,
9024 skippable_errors = ("invalid_grant" , "interaction_required" )):
@@ -124,8 +58,6 @@ def assertCacheWorks(self, result_from_wire):
12458 self .assertNotEqual (result ['access_token' ], result_from_cache ['access_token' ],
12559 "We should get a fresh AT (via RT)" )
12660
127-
128- class UsernamePasswordMixin (object ):
12961 def test_username_password (self ):
13062 self .skipIfNotConfigured ([
13163 "authority" , "client_id" , "username" , "password" , "scope" ])
@@ -135,59 +67,149 @@ def test_username_password(self):
13567 self .config ["username" ], self .config ["password" ],
13668 scopes = self .config .get ("scope" ))
13769 self .assertLoosely (result )
70+ # self.assertEqual(None, result.get("error"), str(result))
13871 self .assertCacheWorks (result )
13972
14073
141- DEFAULT_QUERY = {"mam" : False , "mfa" : False }
74+ CONFIG = os .path .join (os .path .dirname (__file__ ), "config.json" )
75+ @unittest .skipIf (not os .path .exists (CONFIG ), "Optional %s not found" % CONFIG )
76+ class FileBasedTestCase (E2eTestCase ):
77+ def setUp (self ):
78+ with open (CONFIG ) as f :
79+ self .config = json .load (f )
14280
143- # Note: the following semi-parameterized testing approach is inspired from
144- # https://bugs.python.org/msg151444
14581
146- @unittest .skip ("for now" )
147- class AadManagedUserPassTestCase (BaseMixin , UsernamePasswordMixin , unittest .TestCase ):
148- def setUp (self ):
149- self .config = get_lab_user (dict (DEFAULT_QUERY , isFederated = False ))
82+ def get_lab_user (query ): # This API requires no authorization
83+ # Based on https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/LAB.aspx
84+ user = requests .get ("https://api.msidlab.com/api/user" , params = query ).json ()
85+ return { # Mapping lab API response to our simplified configuration format
86+ "authority" : user ["Authority" ][0 ] + user ["Users" ]["tenantId" ],
87+ "client_id" : user ["AppID" ],
88+ "username" : user ["Users" ]["upn" ],
89+ "lab" : {"labname" : user ["Users" ]["upn" ].split ('@' )[1 ].split ('.' )[0 ]}, # :(
90+ "scope" : ["https://graph.microsoft.com/.default" ],
91+ }
15092
151- @unittest .skip ("for now" )
152- class Adfs4FedUserPassTestCase (BaseMixin , UsernamePasswordMixin , unittest .TestCase ):
153- def setUp (self ):
154- self .config = get_lab_user (dict (
93+ def get_lab_app (
94+ env_client_id = "LAB_APP_CLIENT_ID" ,
95+ env_client_secret = "LAB_APP_CLIENT_SECRET" ,
96+ ):
97+ """Returns the lab app as an MSAL confidential client.
98+
99+ Get it from environment variables if defined, otherwise fall back to use MSI.
100+ """
101+ if os .getenv (env_client_id ) and os .getenv (env_client_secret ):
102+ # A shortcut mainly for running tests on developer's local development machine
103+ # or it could be setup on Travis CI
104+ # https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-settings
105+ # Data came from here
106+ # https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Rese.aspx#programmatic-access-info-for-lab-request-api
107+ logger .info ("Using lab app defined by ENV variables %s and %s" ,
108+ env_client_id , env_client_secret )
109+ client_id = os .getenv (env_client_id )
110+ client_secret = os .getenv (env_client_secret )
111+ else :
112+ logger .info ("ENV variables %s and/or %s are not defined. Fall back to MSI." ,
113+ env_client_id , env_client_secret )
114+ # See also https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Programmatically-accessing-LAB-API's.aspx
115+ raise NotImplementedError ("MSI-based mechanism has not been implemented yet" )
116+ return msal .ConfidentialClientApplication (client_id , client_secret ,
117+ authority = "https://login.microsoftonline.com/"
118+ "72f988bf-86f1-41af-91ab-2d7cd011db47" , # Microsoft tenant ID
119+ )
120+
121+ def get_session (lab_app ): # BTW, this infrastructure tests the confidential client flow
122+ logger .info ("Creating session" )
123+ lab_token = lab_app .acquire_token_for_client ("https://request.msidlab.com/.default" )
124+ session = requests .Session ()
125+ session .headers .update ({"Authorization" : "Bearer %s" % lab_token ["access_token" ]})
126+ session .hooks ["response" ].append (lambda r , * args , ** kwargs : r .raise_for_status ())
127+ return session
128+
129+
130+ class LabBasedTestCase (E2eTestCase ):
131+ session = get_session (get_lab_app ()) # It will run even all test cases are skipped
132+ _secrets = {}
133+
134+ @classmethod
135+ def get_lab_user_secret (cls , lab_name = "msidlab4" ):
136+ lab_name = lab_name .lower ()
137+ if lab_name not in cls ._secrets :
138+ logger .info ("Querying lab user password for %s" , lab_name )
139+ # Note: Short link won't work "https://aka.ms/GetLabUserSecret?Secret=%s"
140+ # So we use the official link written in here
141+ # https://microsoft.sharepoint-df.com/teams/MSIDLABSExtended/SitePages/Programmatically-accessing-LAB-API%27s.aspx
142+ url = ("https://request.msidlab.com/api/GetLabUserSecret?code=KpY5uCcoKo0aW8VOL/CUO3wnu9UF2XbSnLFGk56BDnmQiwD80MQ7HA==&Secret=%s"
143+ % lab_name )
144+ resp = cls .session .get (url )
145+ cls ._secrets [lab_name ] = resp .json ()["Value" ]
146+ return cls ._secrets [lab_name ]
147+
148+ @classmethod
149+ def get_lab_user (cls , query ): # The query format is in lab team's Aug 9 email
150+ resp = cls .session .get ("https://user.msidlab.com/api/user" , params = query )
151+ result = resp .json ()[0 ]
152+ return { # Mapping lab API response to our simplified configuration format
153+ "authority" : result ["lab" ]["authority" ] + result ["lab" ]["tenantid" ],
154+ "client_id" : result ["app" ]["objectid" ],
155+ "username" : result ["user" ]["upn" ],
156+ "lab" : result ["lab" ],
157+ "scope" : ["https://graph.microsoft.com/.default" ],
158+ }
159+
160+ DEFAULT_QUERY = {"mam" : False , "mfa" : False }
161+
162+ class AadManagedUserTestCase (LabBasedTestCase ):
163+ @classmethod
164+ def setUpClass (cls ):
165+ cls .config = get_lab_user (dict (DEFAULT_QUERY ,
166+ isFederated = False , # Supposed to find a pure managed user,
167+ # but lab still gives us a [email protected] 168+ ))
169+ cls .config ["password" ] = cls .get_lab_user_secret (cls .config ["lab" ]["labname" ])
170+
171+ class Adfs4FedUserTestCase (LabBasedTestCase ):
172+ @classmethod
173+ def setUpClass (cls ):
174+ cls .config = get_lab_user (dict (
155175 DEFAULT_QUERY , isFederated = True , federationProvider = "ADFSv4" ))
176+ cls .config ["password" ] = cls .get_lab_user_secret (cls .config ["lab" ]["labname" ])
156177
157- @ unittest . skip ( "for now" )
158- class Adfs4ManagedUserPassTestCase ( BaseMixin , UsernamePasswordMixin , unittest . TestCase ):
159- def setUp ( self ):
160- self .config = get_lab_user (dict (
178+ class Adfs4ManagedUserTestCase ( LabBasedTestCase ): # a.k.a. the hybrid
179+ @ classmethod
180+ def setUpClass ( cls ):
181+ cls .config = get_lab_user (dict (
161182 DEFAULT_QUERY , isFederated = False , federationProvider = "ADFSv4" ))
183+ cls .config ["password" ] = cls .get_lab_user_secret (cls .config ["lab" ]["labname" ])
162184
163- @ unittest . skip ( "for now" ) # TODO: Need to pick up the real password
164- class Adfs3FedUserPassTestCase ( BaseMixin , UsernamePasswordMixin , unittest . TestCase ):
165- def setUp ( self ):
166- self .config = get_lab_user (dict (
185+ class Adfs3FedUserTestCase ( LabBasedTestCase ):
186+ @ classmethod
187+ def setUpClass ( cls ):
188+ cls .config = get_lab_user (dict (
167189 DEFAULT_QUERY , isFederated = True , federationProvider = "ADFSv3" ))
168-
169- @unittest .skip ("for now" ) # TODO: Need to pick up the real password
170- class Adfs3ManagedUserPassTestCase (BaseMixin , UsernamePasswordMixin , unittest .TestCase ):
171- def setUp (self ):
172- self .config = get_lab_user (dict (
190+ #cls.config = cls.get_lab_user({
191+ # "MFA": "none", "UserType": "federated", "FederationProvider": "adfsv3"})
192+ cls .config ["password" ] = cls .get_lab_user_secret (cls .config ["lab" ]["labname" ])
193+
194+ class Adfs3ManagedUserTestCase (LabBasedTestCase ): # a.k.a. the hybrid
195+ @classmethod
196+ def setUpClass (cls ):
197+ cls .config = get_lab_user (dict (
173198 DEFAULT_QUERY , isFederated = False , federationProvider = "ADFSv3" ))
199+ cls .config ["password" ] = cls .get_lab_user_secret (cls .config ["lab" ]["labname" ])
174200
175- @ unittest . skip ( "for now" ) # TODO: Need to pick up the real password
176- class Adfs2FedUserPassTestCase ( BaseMixin , UsernamePasswordMixin , unittest . TestCase ):
177- def setUp ( self ):
178- self .config = get_lab_user (dict (
201+ class Adfs2FedUserTestCase ( LabBasedTestCase ):
202+ @ classmethod
203+ def setUpClass ( cls ):
204+ cls .config = get_lab_user (dict (
179205 DEFAULT_QUERY , isFederated = True , federationProvider = "ADFSv2" ))
206+ cls .config ["password" ] = cls .get_lab_user_secret (cls .config ["lab" ]["labname" ])
180207
181208@unittest .skip ("Lab API returns nothing. We might need to switch to beta api" )
182- class Adfs2019FedUserPassTestCase (BaseMixin , UsernamePasswordMixin , unittest .TestCase ):
183- def setUp (self ):
184- self .config = get_lab_user (dict (
209+ class Adfs2019FedUserTestCase (LabBasedTestCase ):
210+ @classmethod
211+ def setUpClass (cls ):
212+ cls .config = get_lab_user (dict (
185213 DEFAULT_QUERY , isFederated = True , federationProvider = "ADFSv2019" ))
186-
187- CONFIG = os .path .join (os .path .dirname (__file__ ), "config.json" )
188- @unittest .skipIf (not os .path .exists (CONFIG ), "Optional %s not found" % CONFIG )
189- class FileBasedTestCase (BaseMixin , UsernamePasswordMixin , unittest .TestCase ):
190- def setUp (self ):
191- with open (CONFIG ) as f :
192- self .config = json .load (f )
214+ cls .config ["password" ] = cls .get_lab_user_secret (cls .config ["lab" ]["labname" ])
193215
0 commit comments