def create_account_external_auth(auth_config, maas_config, bakery_client=None): """Make the user login via external auth to create the first admin.""" if bakery_client is None: bakery_client = httpbakery.Client() maas_url = maas_config['maas_url'].strip('/') failed_msg = '' try: resp = bakery_client.request( 'GET', '{}/accounts/discharge-request/'.format(maas_url)) if resp.status_code != 200: failed_msg = 'request failed with code {}'.format(resp.status_code) except Exception as e: failed_msg = str(e) if failed_msg: print_msg( "An error occurred while waiting for the first user creation: " + failed_msg) return result = resp.json() username = result['username'] if result['is_superuser']: print_msg("Administrator user '{}' created".format(username)) else: admin_group = auth_config['external_auth_admin_group'] message = dedent("""\ A user with username '{username}' has been created, but it's not a superuser. Please log in to MAAS with a user that belongs to the '{admin_group}' group to create an administrator user. """) print_msg(message.format(username=username, admin_group=admin_group))
def legacy_interact(self, client, location, visit_url): '''Implement LegacyInteractor.legacy_interact by obtaining the discharge macaroon using the client's private key ''' agent = self._find_agent(location) pk_encoded = self._auth_info.key.public_key.encode().decode('utf-8') value = { 'username': agent.username, 'public_key': pk_encoded, } # TODO(rogpeppe) use client passed into interact method. client = httpbakery.Client(key=self._auth_info.key) client.cookies.set_cookie( utils.cookie( url=visit_url, name='agent-login', value=base64.urlsafe_b64encode( json.dumps(value).encode('utf-8')).decode('utf-8'), )) resp = requests.get(url=visit_url, cookies=client.cookies, auth=client.auth()) if resp.status_code != 200: raise httpbakery.InteractionError( 'cannot acquire agent macaroon: {}'.format(resp.status_code)) if not resp.json().get('agent-login', False): raise httpbakery.InteractionError('agent login failed')
def test_cookie_domain_host_not_fqdn(self): # See # https://github.com/go-macaroon-bakery/py-macaroon-bakery/issues/53 b = new_bakery('loc', None, None) def handler(*args): GetHandler(b, None, None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() srv_macaroon = b.oven.macaroon( version=bakery.LATEST_VERSION, expiry=AGES, caveats=None, ops=[TEST_OP]) self.assertEquals(srv_macaroon.macaroon.location, 'loc') client = httpbakery.Client() # Note: by using "localhost" instead of the presumably numeric address held # in httpd.server_address, we're triggering the no-FQDN logic in the cookie # code. resp = requests.get( url='http://localhost:' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) resp.raise_for_status() self.assertEquals(resp.text, 'done') except httpbakery.BakeryException: pass # interacion required exception is expected finally: httpd.shutdown() # the cookie has the .local domain appended [cookie] = client.cookies self.assertEqual(cookie.name, 'macaroon-test') self.assertEqual(cookie.domain, 'localhost.local')
def test_single_service_first_party(self): b = new_bakery('loc', None, None) def handler(*args): GetHandler(b, None, None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() srv_macaroon = b.oven.macaroon(version=bakery.LATEST_VERSION, expiry=AGES, caveats=None, ops=[TEST_OP]) self.assertEquals(srv_macaroon.macaroon.location, 'loc') client = httpbakery.Client() client.cookies.set_cookie( requests.cookies.create_cookie( 'macaroon-test', base64.b64encode( json.dumps([srv_macaroon.to_dict().get('m') ]).encode('utf-8')).decode('utf-8'))) resp = requests.get(url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) resp.raise_for_status() self.assertEquals(resp.text, 'done') finally: httpd.shutdown()
def __init__(self, url=API_URL, timeout=DEFAULT_TIMEOUT, verify=True, client=None, cookies=None): """Initializer. @param url The base url to the charmstore API. Defaults to `https://api.jujucharms.com/`. @param timeout How long to wait in seconds before timing out a request; a value of None means no timeout. @param verify Whether to verify the certificate for the charmstore API host. @param client (httpbakery.Client) holds a context for making http requests with macaroons. @param cookies (which act as dict) holds cookies to be sent with the requests. """ super(CharmStore, self).__init__() self.url = url self.verify = verify self.timeout = timeout self.cookies = cookies if client is None: client = httpbakery.Client() self._client = client
def _get_bakery_client(auth_info=None): """Return an httpbakery.Client.""" interaction_methods = None if auth_info is not None: interaction_methods = [AgentInteractor(auth_info)] else: username, password = None, None credentials = os.environ.get('MAAS_CANDID_CREDENTIALS') if credentials: user_pass = credentials.split(':', 1) if len(user_pass) == 2: username, password = user_pass if username and password: def login_with_credentials(visit_url): """Login with Candid's builtin username/password form. This is only intended for use in automated testing. """ resp = requests.get(visit_url) assert resp.status_code == 200 requests.post(resp.url, data={ 'username': username, 'password': password }) interaction_methods = [ httpbakery.WebBrowserInteractor(open=login_with_credentials) ] return httpbakery.Client(interaction_methods=interaction_methods)
def _get_bakery_client(auth_info=None): """Return an httpbakery.Client.""" if auth_info is not None: interactor = AgentInteractor(auth_info) else: interactor = httpbakery.WebBrowserInteractor( open=_candid_login(os.environ.get("MAAS_CANDID_CREDENTIALS"))) return httpbakery.Client(interaction_methods=[interactor])
def test_cookie_with_port(self): client = httpbakery.Client() with HTTMock(first_407_then_200_with_port): with HTTMock(discharge_200): resp = requests.get('http://example.com:8000/someprotecteurl', cookies=client.cookies, auth=client.auth()) resp.raise_for_status() assert 'macaroon-test' in client.cookies.keys()
def test_discharge_with_interaction_required_error(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): return { 'status_code': 401, 'content': { 'Code': httpbakery.ERR_INTERACTION_REQUIRED, 'Message': 'interaction required', 'Info': { 'WaitURL': 'http://0.1.2.3/', 'VisitURL': 'http://0.1.2.3/', }, } } def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() class MyInteractor(httpbakery.LegacyInteractor): def legacy_interact(self, ctx, location, visit_url): raise httpbakery.InteractionError('cannot visit') def interact(self, ctx, location, interaction_required_err): pass def kind(self): return httpbakery.WEB_BROWSER_INTERACTION_KIND client = httpbakery.Client(interaction_methods=[MyInteractor()]) with HTTMock(discharge): with self.assertRaises(httpbakery.InteractionError): requests.get('http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) finally: httpd.shutdown()
def test_discharge(self): client = httpbakery.Client() with HTTMock(first_407_then_200), HTTMock(discharge_200): resp = requests.get(ID_PATH, cookies=client.cookies, auth=client.auth()) resp.raise_for_status() assert 'macaroon-test' in client.cookies.keys() self.assert_cookie_security(client.cookies, 'macaroon-test', secure=False)
def __init__(self, url, auth_info): self._url = url.rstrip('/') self._auth_info = auth_info interaction_methods = None if auth_info is not None: # if auth info is specified, use the default interaction (most # likely the browser-based one) interaction_methods = [AgentInteractor(self._auth_info)] self._client = httpbakery.Client( interaction_methods=interaction_methods)
def get_token(): client = httpbakery.Client() resp = client.request( 'POST', '{}/account/?op=create_authorisation_token'.format(url), verify=not insecure) if resp.status_code != 200: raise LoginError('Login failed: {}'.format(resp.text)) result = resp.json() return '{consumer_key}:{token_key}:{token_secret}'.format(**result)
def authentication(request, payload): url = 'https://ubuntu.com/security/releases' client = httpbakery.Client(cookies=MozillaCookieJar(".login")) if os.path.exists(client.cookies.filename): client.cookies.load(ignore_discard=True) response = client.request(request, url=url, json=payload) client.cookies.save(ignore_discard=True) print(response, response.text)
def __init__( self, *, user_agent: str = agent.get_user_agent(), bakery_client=httpbakery.Client(), ) -> None: super().__init__(user_agent=user_agent) self.bakery_client = bakery_client self._conf = CandidConfig() self._conf_save = True
def test_407_then_no_interaction_methods(self): client = httpbakery.Client(interaction_methods=[]) with HTTMock(first_407_then_200), HTTMock(discharge_401): with self.assertRaises(httpbakery.InteractionError) as exc: requests.get( ID_PATH, cookies=client.cookies, auth=client.auth(), ) self.assertEqual(str(exc.exception), 'cannot start interactive session: interaction ' 'required but not possible')
def bakery_client_for_controller(self, controller_name): '''Make a copy of the bakery client with a the appropriate controller's cookiejar in it. ''' bakery_client = self.bakery_client if bakery_client: bakery_client = copy.copy(bakery_client) else: bakery_client = httpbakery.Client() bakery_client.cookies = self.jujudata.cookies_for_controller( controller_name) return bakery_client
def test_407_then_401_on_discharge(self, mock_open): client = httpbakery.Client() with HTTMock(first_407_then_200), HTTMock(discharge_401), \ HTTMock(wait_after_401): resp = requests.get( ID_PATH, cookies=client.cookies, auth=client.auth(), ) resp.raise_for_status() mock_open.assert_called_once_with(u'http://example.com/visit', new=1) assert 'macaroon-test' in client.cookies.keys()
def test_expiry_cookie_is_set(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } ages = datetime.datetime.utcnow() + datetime.timedelta(days=1) def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, ages, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): resp = requests.get( url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) resp.raise_for_status() m = bakery.Macaroon.from_dict(json.loads( base64.b64decode(client.cookies.get('macaroon-test')).decode('utf-8'))[0]) t = checkers.macaroons_expiry_time( checkers.Namespace(), [m.macaroon]) self.assertEquals(ages, t) self.assertEquals(resp.text, 'done') finally: httpd.shutdown()
def authentication(request, url, payload): """ Authenticate with Macaroons in order to use Webteam API """ client = httpbakery.Client(cookies=MozillaCookieJar(".login")) if os.path.exists(client.cookies.filename): client.cookies.load(ignore_discard=True) response = client.request(request, url=url, json=payload) client.cookies.save(ignore_discard=True) print(response, response.text)
def __init__(self, *, user_agent: str = agent.get_user_agent(), bakery_client=None) -> None: super().__init__(user_agent=user_agent) if bakery_client is None: self.bakery_client = httpbakery.Client( interaction_methods=[WebBrowserWaitingInteractor()]) else: self.bakery_client = bakery_client self._conf = CandidConfig() self._conf_save = True
def authentication(method, url, payload): """ Authenticate with Macaroons in order to use Webteam API """ client = httpbakery.Client(cookies=MozillaCookieJar(os.path.expanduser("~/.ubuntu.com.login"))) if os.path.exists(client.cookies.filename): client.cookies.load(ignore_discard=True) response = client.request(method, url=url, json=payload) client.cookies.save(ignore_discard=True) return response
def __init__(self, url, timeout=DEFAULT_TIMEOUT, client=None): """Initializer. @param url The url to the Terms Service API. @param timeout How long to wait in seconds before timing out a request; a value of None means no timeout. @param client (httpbakery.Client) holds a context for making http requests with macaroons. """ self.url = ensure_trailing_slash(url) + TERMS_VERSION + '/' self.timeout = timeout if client is None: client = httpbakery.Client() self._client = client
def test_407_then_error_on_wait(self, mock_open): client = httpbakery.Client() with HTTMock(first_407_then_200), HTTMock(discharge_401),\ HTTMock(wait_on_error): with self.assertRaises(httpbakery.InteractionError) as exc: requests.get( ID_PATH, cookies=client.cookies, auth=client.auth(), ) self.assertEqual( str(exc.exception), 'cannot start interactive session: cannot get ' 'http://example.com/wait') mock_open.assert_called_once_with(u'http://example.com/visit', new=1)
def get_token(): client = httpbakery.Client() resp = client.request( 'POST', '{}/account/?op=create_authorisation_token'.format(url), verify=not insecure) if resp.status_code == HTTPStatus.UNAUTHORIZED: # if the auteentication with Candid fails, an exception is raised # above so we don't get here raise MacaroonLoginNotSupported( 'Macaroon authentication not supported') if resp.status_code != HTTPStatus.OK: raise LoginError('Login failed: {}'.format(resp.text)) result = resp.json() return '{consumer_key}:{token_key}:{token_secret}'.format(**result)
def test_expiry_cookie_set_in_past(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } ages = datetime.datetime.utcnow() - datetime.timedelta(days=1) def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, ages, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): with self.assertRaises(httpbakery.BakeryException) as ctx: requests.get( url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) self.assertEqual(ctx.exception.args[0], 'too many (3) discharge requests') finally: httpd.shutdown()
async def test_macaroon_auth(event_loop): auth_info, username = agent_auth_info() # Create a bakery client that can do agent authentication. client = httpbakery.Client( key=auth_info.key, interaction_methods=[agent.AgentInteractor(auth_info)], ) async with base.CleanModel(bakery_client=client) as m: async with await m.get_controller() as c: await c.grant_model(username, m.info.uuid, 'admin') async with Model( jujudata=NoAccountsJujuData(m._connector.jujudata), bakery_client=client, ): pass
def test_too_many_discharge(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): wrong_macaroon = bakery.Macaroon(root_key=b'some key', id=b'xxx', location='some other location', version=bakery.VERSION_0) return { 'status_code': 200, 'content': { 'Macaroon': wrong_macaroon.to_dict() } } def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): with self.assertRaises(httpbakery.BakeryException) as ctx: requests.get(url='http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]), cookies=client.cookies, auth=client.auth()) self.assertEqual(ctx.exception.args[0], 'too many (3) discharge requests') finally: httpd.shutdown()
async def connect(self, **kwargs): """Connect to an arbitrary Juju model. kwargs are passed through to Connection.connect() """ kwargs.setdefault('loop', self.loop) kwargs.setdefault('max_frame_size', self.max_frame_size) kwargs.setdefault('bakery_client', self.bakery_client) if 'macaroons' in kwargs: if not kwargs['bakery_client']: kwargs['bakery_client'] = httpbakery.Client() if not kwargs['bakery_client'].cookies: kwargs['bakery_client'].cookies = GoCookieJar() jar = kwargs['bakery_client'].cookies for macaroon in kwargs.pop('macaroons'): jar.set_cookie(go_to_py_cookie(macaroon)) self._connection = await Connection.connect(**kwargs)
def test_407_then_unknown_interaction_methods(self): class UnknownInteractor(httpbakery.Interactor): def kind(self): return 'unknown' client = httpbakery.Client(interaction_methods=[UnknownInteractor()]) with HTTMock(first_407_then_200), HTTMock(discharge_401): with self.assertRaises(httpbakery.InteractionError) as exc: requests.get( ID_PATH, cookies=client.cookies, auth=client.auth(), ) self.assertEqual( str(exc.exception), 'cannot start interactive session: no methods supported; ' 'supported [unknown]; provided [interactive]')
def test_single_service_third_party(self): class _DischargerLocator(bakery.ThirdPartyLocator): def __init__(self): self.key = bakery.generate_key() def third_party_info(self, loc): if loc == 'http://1.2.3.4': return bakery.ThirdPartyInfo( public_key=self.key.public_key, version=bakery.LATEST_VERSION, ) d = _DischargerLocator() b = new_bakery('loc', d, None) @urlmatch(path='.*/discharge') def discharge(url, request): qs = parse_qs(request.body) content = {q: qs[q][0] for q in qs} m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd) return { 'status_code': 200, 'content': { 'Macaroon': m.to_dict() } } def handler(*args): GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args) try: httpd = HTTPServer(('', 0), handler) server_url = 'http://' + httpd.server_address[0] + ':' + str(httpd.server_address[1]) thread = threading.Thread(target=httpd.serve_forever) thread.start() client = httpbakery.Client() with HTTMock(discharge): resp = requests.get( url=server_url, cookies=client.cookies, auth=client.auth()) resp.raise_for_status() self.assertEquals(resp.text, 'done') finally: httpd.shutdown()