Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit978d8c2

Browse files
committed
Merge pull requestsigmavirus24#167 from sigmavirus24/feature-two-factor-auth
Feature: Two factor auth
2 parents5b2a9f1 +824e4d5 commit978d8c2

File tree

11 files changed

+176
-16
lines changed

11 files changed

+176
-16
lines changed

‎.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ python:
66
-3.3
77
-pypy
88
# # command to run tests, e.g. python setup.py test
9+
before_script:pip install -r dev-requirements.txt
910
script:make travis
1011
notifications:
1112
on_success:change

‎HISTORY.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
History/Changelog
22
=================
33

4+
0.8.0: 2013-xx-xx
5+
-----------------
6+
7+
- Use Betamax to start recording integration tests
8+
9+
- Add support for Releases API
10+
11+
- Add support for Feeds API
12+
13+
- Add support for Two-Factor Authentication via the API
14+
15+
- Switch to requests >= 2.0
16+
417
0.7.1: 2013-09-30
518
-----------------
619

‎dev-requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
requests==1.2.3
1+
requests>=2.0.0,<=3.0.0
22
uritemplate.py==0.2.0
33
#coverage==3.5.2
44
mock==1.0.1
55
pytest==2.3.5
66
wheel==0.21.0
7-
betamax==0.1.5
7+
git+git://github.com/sigmavirus24/betamax

‎github3/github.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,8 @@ def key(self, id_num):
821821
json=self._json(self._get(url),200)
822822
returnKey(json,self)ifjsonelseNone
823823

824-
deflogin(self,username=None,password=None,token=None):
824+
deflogin(self,username=None,password=None,token=None,
825+
two_factor_callback=None):
825826
"""Logs the user into GitHub for protected API calls.
826827
827828
:param str username: (optional)
@@ -833,6 +834,9 @@ def login(self, username=None, password=None, token=None):
833834
eliftoken:
834835
self._session.token_auth(token)
835836

837+
# The Session method handles None for free.
838+
self._session.two_factor_auth_callback(two_factor_callback)
839+
836840
defmarkdown(self,text,mode='',context='',raw=False):
837841
"""Render an arbitrary markdown document.
838842

‎github3/session.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
importrequests
22

3+
fromcollectionsimportCallable
34
fromgithub3import__version__
45
fromloggingimportgetLogger
56

67
__url_cache__= {}
78
__logs__=getLogger(__package__)
89

910

11+
defrequires_2fa(response):
12+
if (response.status_code==401and'X-GitHub-OTP'inresponse.headers
13+
and'required'inresponse.headers['X-GitHub-OTP']):
14+
returnTrue
15+
returnFalse
16+
17+
1018
classGitHubSession(requests.Session):
1119
def__init__(self):
1220
super(GitHubSession,self).__init__()
@@ -21,6 +29,7 @@ def __init__(self):
2129
'User-Agent':'github3.py/{0}'.format(__version__),
2230
})
2331
self.base_url='https://api.github.com'
32+
self.two_factor_auth_cb=None
2433

2534
defbasic_auth(self,username,password):
2635
"""Set the Basic Auth credentials on this Session.
@@ -45,6 +54,43 @@ def build_url(self, *args, **kwargs):
4554
__url_cache__[key]='/'.join(parts)
4655
return__url_cache__[key]
4756

57+
defhandle_two_factor_auth(self,args,kwargs):
58+
headers=kwargs.pop('headers', {})
59+
headers.update({
60+
'X-GitHub-OTP':str(self.two_factor_auth_cb())
61+
})
62+
kwargs.update(headers=headers)
63+
returnsuper(GitHubSession,self).request(*args,**kwargs)
64+
65+
defoauth2_auth(self,client_id,client_secret):
66+
"""Use OAuth2 for authentication.
67+
68+
It is suggested you install requests-oauthlib to use this.
69+
70+
:param str client_id: Client ID retrieved from GitHub
71+
:param str client_secret: Client secret retrieved from GitHub
72+
"""
73+
raiseNotImplementedError('These features are not implemented yet')
74+
75+
defrequest(self,*args,**kwargs):
76+
response=super(GitHubSession,self).request(*args,**kwargs)
77+
ifrequires_2fa(response)andself.two_factor_auth_cb:
78+
# No need to flatten and re-collect the args in
79+
# handle_two_factor_auth
80+
new_response=self.handle_two_factor_auth(args,kwargs)
81+
new_response.history.append(response)
82+
response=new_response
83+
returnresponse
84+
85+
deftwo_factor_auth_callback(self,callback):
86+
ifnotcallback:
87+
return
88+
89+
ifnotisinstance(callback,Callable):
90+
raiseValueError('Your callback should be callable')
91+
92+
self.two_factor_auth_cb=callback
93+
4894
deftoken_auth(self,token):
4995
"""Use an application token for authentication.
5096
@@ -57,13 +103,3 @@ def token_auth(self, token):
57103
self.headers.update({
58104
'Authorization':'token {0}'.format(token)
59105
})
60-
61-
defoauth2_auth(self,client_id,client_secret):
62-
"""Use OAuth2 for authentication.
63-
64-
It is suggested you install requests-oauthlib to use this.
65-
66-
:param str client_id: Client ID retrieved from GitHub
67-
:param str client_secret: Client secret retrieved from GitHub
68-
"""
69-
raiseNotImplementedError('These features are not implemented yet')

‎setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"github3.issues",
1717
]
1818

19-
kwargs['tests_require']= ['mock==1.0.1','betamax==0.1.5','pytest']
19+
kwargs['tests_require']= ['mock==1.0.1','betamax','pytest']
2020
ifsys.version_info< (3,0):
2121
kwargs['tests_require'].append('unittest2==0.5.1')
2222
packages.append('tests')
@@ -25,7 +25,7 @@
2525
os.system("python setup.py bdist_wheel sdist upload")
2626
sys.exit()
2727

28-
requires.extend(["requests >=1.2.3","uritemplate.py >= 0.2.0"])
28+
requires.extend(["requests >=2.0","uritemplate.py >= 0.2.0"])
2929

3030
__version__=''
3131
withopen('github3/__init__.py','r')asfd:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"http_interactions": [{"request": {"body": "", "headers": {"Accept-Encoding": "gzip, deflate, compress", "Accept": "application/vnd.github.v3.full+json", "User-Agent": "github3.py/0.8.0", "Accept-Charset": "utf-8", "Content-Type": "application/json", "Authorization": "Basic <BASIC_AUTH>"}, "method": "GET", "uri": "https://api.github.com/users/sigmavirus24"}, "response": {"body": {"string": "{\"message\":\"Must specify two-factor authentication OTP code.\",\"documentation_url\":\"http://developer.github.com/v3/auth#working-with-two-factor-authentication\"}", "encoding": "utf-8"}, "headers": {"status": "401 Unauthorized", "x-ratelimit-remaining": "57", "x-github-media-type": "github.v3; param=full; format=json", "x-content-type-options": "nosniff", "access-control-expose-headers": "ETag, Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes", "x-github-request-id": "48A0B729:5402:3028482:52969FEC", "x-github-otp": "required; app", "content-length": "159", "server": "GitHub.com", "x-ratelimit-limit": "60", "access-control-allow-credentials": "true", "date": "Thu, 28 Nov 2013 01:44:12 GMT", "access-control-allow-origin": "*", "content-type": "application/json; charset=utf-8", "x-ratelimit-reset": "1385606471"}, "url": "https://api.github.com/users/sigmavirus24", "status_code": 401}, "recorded_at": "2013-11-28T01:44:13"}, {"request": {"body": "", "headers": {"Accept-Encoding": "gzip, deflate, compress", "X-GitHub-OTP": "862478", "Accept": "application/vnd.github.v3.full+json", "User-Agent": "github3.py/0.8.0", "Accept-Charset": "utf-8", "Content-Type": "application/json", "Authorization": "Basic <BASIC_AUTH>"}, "method": "GET", "uri": "https://api.github.com/users/sigmavirus24"}, "response": {"body": {"base64_string": "H4sIAAAAAAAAA51UXWvbMBT9K8XQPSWRvxI7hrIvGPRhbx2DvYRrSXZEFctIcrIsdL99R07ataFQUgjE2Pece+65H4dIm1Z1URU51W5oq+zg0jyaREpEVZrHZRZPItqSJ7sarEbc2vveVYwls9YeP8y42bDjI+NJXmbzhSiXzTKdy8UyTYqyljIRy4x4U3wUNyPBdfb5Ov2GnxKy84qbzs1a5ddDHdjwPqciKRZ5nOVckCyLIi/zWhZpkZQ1zdPFrO/aD/bmN6Q+6lgFzdFbCgB4WQj16llqNjhpHTtzY+03+qz+/2rPgxujtdmB5QzxZiL2hITI47Pq2neyAHlgxq8lGoeSHoJRyvnLRY2oAwt/cDjwOEyDleJiYSccZO06KDowK3szEg6141b1XmEOLqd9jgabsS116g+9jw1oB5Ig7XIpIwpoucVUXw4/wg6st2pLfB+ssZJLtYXZ76Q8w4PR73uJPfmBoQjWKy9XJDbhBjSknZxEHW1CwC11V1+NFeS4QSDWsqdujw9fZCf2O2PvQ6U17sfpKuAo7HY77G+r1bjG7C8WeyRgY9gEx4aPbQHiOwnlTDe5+nkbDNuQCtcFu9w05LnRUNSa7SccJaUDGYLWykqqNcSdlNbKRFU3aD2J+qHWiq+ODajm5eMCYQ2jaoEj9rRPuGsLlAMqD1vJI2saJ/E0zqdJfBfnVVxUWfoL+YZevIjJpkkyTYu7NKnyrMrmIcYbT3o1NgxGntIjXRhx8cp7FH2PhaQWVWS4Y3lwVmuqjSVvgliAe02w6PDYicZKGVrVEw+ouEhjBL0CO1Px8PAP0MDI0N8FAAA=", "encoding": "utf-8"}, "headers": {"status": "200 OK", "x-ratelimit-remaining": "4930", "x-github-media-type": "github.v3; param=full; format=json", "x-content-type-options": "nosniff", "access-control-expose-headers": "ETag, Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes", "transfer-encoding": "chunked", "x-github-request-id": "48A0B729:5402:3028B0A:52969FEC", "content-encoding": "gzip", "vary": "Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding", "server": "GitHub.com", "cache-control": "private, max-age=60, s-maxage=60", "last-modified": "Wed, 27 Nov 2013 21:43:35 GMT", "x-ratelimit-limit": "5000", "etag": "\"678c237081785fa8c850d78a6779b7aa\"", "access-control-allow-credentials": "true", "date": "Thu, 28 Nov 2013 01:44:16 GMT", "access-control-allow-origin": "*", "content-type": "application/json; charset=utf-8", "x-ratelimit-reset": "1385603853"}, "url": "https://api.github.com/users/sigmavirus24", "status_code": 200}, "recorded_at": "2013-11-28T01:44:16"}], "recorded_with": "betamax"}

‎tests/integration/test_session.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
importgithub3
2+
3+
from .helperimportIntegrationHelper
4+
5+
6+
classTestGitHubSession(IntegrationHelper):
7+
deftest_two_factor_authentication_works(self):
8+
two_factor_auth=lambda:'862478'
9+
self.basic_login()
10+
self.gh.login(two_factor_callback=two_factor_auth)
11+
12+
cassette_name=self.cassette_name('two_factor_authentication')
13+
assertisinstance(self.session,github3.session.GitHubSession)
14+
15+
match= ['method','uri','headers']
16+
withself.recorder.use_cassette(cassette_name,
17+
match_requests_on=match):
18+
r=self.session.get('https://api.github.com/users/sigmavirus24')
19+
assertr.status_code==200

‎tests/unit/helper.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ def create_session_mock(self, *args):
3535

3636
defsetUp(self):
3737
self.session=self.create_session_mock()
38-
self.instance=self.described_class(self.example_data,self.session)
38+
ifself.example_data:
39+
self.instance=self.described_class(self.example_data,
40+
self.session)
41+
else:
42+
self.instance=self.described_class()
43+
self.instance._session=self.session
3944
# Proxy the build_url method to the class so it can build the URL and
4045
# we can assert things about the call that will be attempted to the
4146
# internet

‎tests/unit/test_github.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
fromgithub3.githubimportGitHub
2+
3+
from .helperimportUnitHelper
4+
5+
6+
classTestGitHub(UnitHelper):
7+
described_class=GitHub
8+
example_data=None
9+
10+
deftest_two_factor_login(self):
11+
self.instance.login('username','password',
12+
two_factor_callback=lambda*args:'foo')
13+
14+
deftest_can_login_without_two_factor_callback(self):
15+
self.instance.login('username','password')
16+
self.instance.login(token='token')

‎tests/unit/test_github_session.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
importpytest
22

33
importrequests
4+
45
fromgithub3importsession
6+
frommockimportpatch,Mock
57

68

79
classTestGitHubSession:
@@ -70,6 +72,49 @@ def test_basic_login(self):
7072
s.basic_auth('username','password')
7173
asserts.auth== ('username','password')
7274

75+
@patch.object(requests.Session,'request')
76+
deftest_handle_two_factor_auth(self,request_mock):
77+
"""Test the method that handles getting the 2fa code"""
78+
s=self.build_session()
79+
s.two_factor_auth_callback(lambda:'fake')
80+
args= ('GET','http://example.com')
81+
s.handle_two_factor_auth(args, {})
82+
request_mock.assert_called_once_with(
83+
*args,
84+
headers={'X-GitHub-OTP':'fake'}
85+
)
86+
87+
@patch.object(requests.Session,'request')
88+
deftest_request_ignores_responses_that_do_not_require_2fa(self,
89+
request_mock):
90+
"""Test that request does not try to handle 2fa when it should not"""
91+
response=Mock()
92+
response.configure_mock(status_code=200,headers={})
93+
request_mock.return_value=response
94+
s=self.build_session()
95+
s.two_factor_auth_callback(lambda:'fake')
96+
r=s.get('http://example.com')
97+
assertrisresponse
98+
request_mock.assert_called_once_with(
99+
'GET','http://example.com',allow_redirects=True
100+
)
101+
102+
@patch.object(requests.Session,'request')
103+
deftest_creates_history_while_handling_2fa(self,request_mock):
104+
"""Test that the overridden request method will create history"""
105+
response=Mock()
106+
response.configure_mock(
107+
status_code=401,
108+
headers={'X-GitHub-OTP':'required;2fa'},
109+
history=[]
110+
)
111+
request_mock.return_value=response
112+
s=self.build_session()
113+
s.two_factor_auth_callback(lambda:'fake')
114+
r=s.get('http://example.com')
115+
assertlen(r.history)!=0
116+
assertrequest_mock.call_count==2
117+
73118
deftest_token_auth(self):
74119
"""Test that token auth will work with a valid token"""
75120
s=self.build_session()
@@ -84,6 +129,26 @@ def test_token_auth_does_not_use_falsey_values(self):
84129
s.token_auth(token)
85130
assert'Authorization'notins.headers
86131

132+
deftest_two_factor_auth_callback_handles_None(self):
133+
s=self.build_session()
134+
asserts.two_factor_auth_cbisNone
135+
s.two_factor_auth_callback(None)
136+
asserts.two_factor_auth_cbisNone
137+
138+
deftest_two_factor_auth_callback_checks_for_Callable(self):
139+
s=self.build_session()
140+
asserts.two_factor_auth_cbisNone
141+
withpytest.raises(ValueError):
142+
s.two_factor_auth_callback(1)
143+
144+
deftest_two_factor_auth_callback_accepts_a_Callable(self):
145+
s=self.build_session()
146+
asserts.two_factor_auth_cbisNone
147+
# You have to have a sense of humor ;)
148+
not_so_anonymous=lambda*args:'foo'
149+
s.two_factor_auth_callback(not_so_anonymous)
150+
asserts.two_factor_auth_cbisnot_so_anonymous
151+
87152
deftest_oauth2_auth(self):
88153
"""Test that oauth2 authentication works
89154

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp