def attach_with_token( cfg: config.UAConfig, token: str, allow_enable: bool ) -> None: """ Common functionality to take a token and attach via contract backend :raise UrlError: On unexpected connectivity issues to contract server or inability to access identity doc from metadata service. :raise ContractAPIError: On unexpected errors when talking to the contract server. """ from uaclient.jobs.update_messaging import update_apt_and_motd_messages try: contract.request_updated_contract( cfg, token, allow_enable=allow_enable ) except exceptions.UrlError as exc: # Persist updated status in the event of partial attach ua_status.status(cfg=cfg) update_apt_and_motd_messages(cfg) raise exc except exceptions.UserFacingError as exc: # Persist updated status in the event of partial attach ua_status.status(cfg=cfg) update_apt_and_motd_messages(cfg) raise exc current_iid = identity.get_instance_id() if current_iid: cfg.write_cache("instance-id", current_iid) update_apt_and_motd_messages(cfg)
def can_enable(self, silent: bool = False) -> bool: """ Report whether or not enabling is possible for the entitlement. :param silent: if True, suppress output """ if self.is_access_expired(): logging.debug( "Updating contract on service '%s' expiry", self.name ) contract.request_updated_contract(self.cfg) if not self.contract_status() == ContractStatus.ENTITLED: if not silent: print(status.MESSAGE_UNENTITLED_TMPL.format(title=self.title)) return False application_status, _ = self.application_status() if application_status != status.ApplicationStatus.DISABLED: if not silent: print( status.MESSAGE_ALREADY_ENABLED_TMPL.format( title=self.title ) ) return False applicability_status, details = self.applicability_status() if applicability_status == status.ApplicabilityStatus.INAPPLICABLE: if not silent: print(details) return False return True
def refresh_contract(cfg): try: contract.request_updated_contract(cfg) except exceptions.UrlError as exc: logging.exception(exc) logging.warning(messages.REFRESH_CONTRACT_FAILURE) sys.exit(1)
def _attach_with_token(cfg: config.UAConfig, token: str, allow_enable: bool) -> int: """Common functionality to take a token and attach via contract backend""" try: contract.request_updated_contract(cfg, token, allow_enable=allow_enable) except util.UrlError as exc: with util.disable_log_to_console(): logging.exception(exc) print(ua_status.MESSAGE_ATTACH_FAILURE) cfg.status() # Persist updated status in the event of partial attach return 1 except exceptions.UserFacingError as exc: logging.warning(exc.msg) cfg.status() # Persist updated status in the event of partial attach return 1 contract_name = cfg.machine_token["machineTokenInfo"]["contractInfo"][ "name"] print( ua_status.MESSAGE_ATTACH_SUCCESS_TMPL.format( contract_name=contract_name)) action_status(args=None, cfg=cfg) return 0
def test_user_facing_error_on_service_token_refresh_failure( self, client, get_machine_id, FakeConfig ): """When attaching, error on any failed specific service refresh.""" machine_token = { "machineToken": "mToken", "machineTokenInfo": { "contractInfo": { "id": "cid", "resourceEntitlements": [ {"entitled": True, "type": "ent2"}, {"entitled": True, "type": "ent1"}, ], } }, } def fake_contract_client(cfg): fake_client = FakeContractClient(cfg) fake_client._responses = {self.refresh_route: machine_token} return fake_client client.side_effect = fake_contract_client cfg = FakeConfig.for_attached_machine(machine_token=machine_token) with mock.patch(M_PATH + "process_entitlement_delta") as m_process: m_process.side_effect = ( exceptions.UserFacingError("broken ent1"), exceptions.UserFacingError("broken ent2"), ) with pytest.raises(exceptions.UserFacingError) as exc: request_updated_contract(cfg) assert MESSAGE_ATTACH_FAILURE_DEFAULT_SERVICES == str(exc.value)
def test_invalid_token_user_facing_error_on_invalid_token_refresh_failure( self, client, get_machine_id ): """When attaching, invalid token errors result in proper user error.""" def fake_contract_client(cfg): fake_client = FakeContractClient(cfg) fake_client._responses = { API_V1_CONTEXT_MACHINE_TOKEN: ContractAPIError( util.UrlError( "Server error", code=500, url="http://me", headers={} ), error_response={ "message": "invalid token: checksum error" }, ) } return fake_client client.side_effect = fake_contract_client cfg = FakeConfig() with pytest.raises(exceptions.UserFacingError) as exc: request_updated_contract(cfg, contract_token="yep") assert MESSAGE_ATTACH_INVALID_TOKEN == str(exc.value)
def test_invalid_token_user_facing_error_on_invalid_token_refresh_failure( self, client, get_machine_id, FakeConfig, error_code, error_msg, error_response, ): """When attaching, invalid token errors result in proper user error.""" def fake_contract_client(cfg): fake_client = FakeContractClient(cfg) fake_client._responses = { API_V1_CONTEXT_MACHINE_TOKEN: exceptions.ContractAPIError( exceptions.UrlError( "Server error", code=error_code, url="http://me", headers={}, ), error_response=json.loads( error_response, cls=util.DatetimeAwareJSONDecoder), ) } return fake_client client.side_effect = fake_contract_client cfg = FakeConfig() with pytest.raises(exceptions.UserFacingError) as exc: request_updated_contract(cfg, contract_token="yep") assert error_msg.msg == str(exc.value.msg)
def action_attach(args, cfg): if cfg.is_attached: print("This machine is already attached to '{}'.".format( cfg.accounts[0]["name"])) return 0 if os.getuid() != 0: raise exceptions.NonRootUserError() contract_token = args.token if not contract_token: print("No valid contract token available") return 1 try: contract.request_updated_contract(cfg, contract_token, allow_enable=args.auto_enable) except util.UrlError as exc: with util.disable_log_to_console(): logging.exception(exc.msg) print( ua_status.MESSAGE_ATTACH_FAILURE_TMPL.format(url=cfg.contract_url)) return 1 except exceptions.UserFacingError as exc: logging.warning(exc.msg) action_status(args=None, cfg=cfg) return 1 contract_name = cfg.machine_token["machineTokenInfo"]["contractInfo"][ "name"] print( ua_status.MESSAGE_ATTACH_SUCCESS_TMPL.format( contract_name=contract_name)) action_status(args=None, cfg=cfg) return 0
def action_refresh(args, cfg): try: contract.request_updated_contract(cfg) except util.UrlError as exc: with util.disable_log_to_console(): logging.exception(exc) raise exceptions.UserFacingError(ua_status.MESSAGE_REFRESH_FAILURE) print(ua_status.MESSAGE_REFRESH_SUCCESS) return 0
def action_enable(args, cfg): """Perform the enable action on a named entitlement. @return: 0 on success, 1 otherwise """ print(ua_status.MESSAGE_REFRESH_ENABLE) try: contract.request_updated_contract(cfg) except (util.UrlError, exceptions.UserFacingError): # Inability to refresh is not a critical issue during enable logging.debug(ua_status.MESSAGE_REFRESH_FAILURE, exc_info=True) return 0 if _perform_enable(args.name, cfg) else 1
def test_attached_config_and_contract_token_runtime_error(self, client): """When attached, error if called with a contract_token.""" def fake_contract_client(cfg): return FakeContractClient(cfg) client.side_effect = fake_contract_client cfg = FakeConfig.for_attached_machine() with pytest.raises(RuntimeError) as exc: request_updated_contract(cfg, contract_token="something") expected_msg = ( "Got unexpected contract_token on an already attached machine") assert expected_msg == str(exc.value)
def action_enable(args, cfg, **kwargs): """Perform the enable action on a named entitlement. @return: 0 on success, 1 otherwise """ print(ua_status.MESSAGE_REFRESH_ENABLE) try: contract.request_updated_contract(cfg) except (util.UrlError, exceptions.UserFacingError): # Inability to refresh is not a critical issue during enable logging.debug(ua_status.MESSAGE_REFRESH_FAILURE, exc_info=True) names = getattr(args, "service", []) entitlements_found, entitlements_not_found = get_valid_entitlement_names( names ) ret = True for entitlement in entitlements_found: try: ret &= _perform_enable( entitlement, cfg, assume_yes=args.assume_yes, allow_beta=args.beta, ) except exceptions.BetaServiceError: entitlements_not_found.append(entitlement) except exceptions.UserFacingError as e: print(e) if entitlements_not_found: if args.beta: valid_names = entitlements.ALL_ENTITLEMENTS_STR else: valid_names = entitlements.RELEASED_ENTITLEMENTS_STR service_msg = "\n".join( textwrap.wrap( "Try " + valid_names, width=80, break_long_words=False ) ) tmpl = ua_status.MESSAGE_INVALID_SERVICE_OP_FAILURE_TMPL raise exceptions.UserFacingError( tmpl.format( operation="enable", name=", ".join(entitlements_not_found), service_msg=service_msg, ) ) return 0 if ret else 1
def test_attached_config_refresh_errors_on_expired_contract( self, client, get_machine_id, process_entitlement_delta): """Error when refreshing contract parses an expired contract token.""" machine_token = { "machineToken": "mToken", "machineTokenInfo": { "contractInfo": { "effectiveTo": "2018-07-18T00:00:00Z", # Expired date "id": "cid", "resourceEntitlements": [ { "entitled": False, "type": "ent2" }, { "entitled": True, "type": "ent1" }, ], } }, } def fake_contract_client(cfg): client = FakeContractClient(cfg) # Note ent2 access route is not called client._responses = { self.refresh_route: machine_token, self.access_route_ent1: { "entitlement": { "entitled": True, "type": "ent1", "new": "newval", } }, } return client client.side_effect = fake_contract_client cfg = FakeConfig.for_attached_machine(machine_token=machine_token) with pytest.raises(exceptions.UserFacingError) as exc: request_updated_contract(cfg) assert MESSAGE_CONTRACT_EXPIRED_ERROR == str(exc.value) assert machine_token == cfg.read_cache("machine-token") # No deltas are processed when contract is expired assert 0 == process_entitlement_delta.call_count
def test_attached_config_refresh_machine_token_and_services( self, client, get_machine_id, process_entitlement_delta, FakeConfig ): """When attached, refresh machine token and entitled services. Processing service deltas are processed in a sorted order based on name to ensure operations occur the same regardless of dict ordering. """ # resourceEntitlements specifically ordered reverse alphabetically # to ensure proper sorting for process_contract_delta calls below machine_token = { "machineToken": "mToken", "machineTokenInfo": { "contractInfo": { "id": "cid", "resourceEntitlements": [ {"entitled": False, "type": "ent2"}, {"entitled": True, "type": "ent1"}, ], } }, } new_token = copy.deepcopy(machine_token) new_token["machineTokenInfo"]["contractInfo"]["resourceEntitlements"][ 1 ]["new"] = "newval" def fake_contract_client(cfg): client = FakeContractClient(cfg) client._responses = {self.refresh_route: new_token} return client client.side_effect = fake_contract_client cfg = FakeConfig.for_attached_machine(machine_token=machine_token) assert None is request_updated_contract(cfg) assert new_token == cfg.read_cache("machine-token") # Deltas are processed in a sorted fashion so that if enableByDefault # is true, the order of enablement operations is the same regardless # of dict key ordering. process_calls = [ mock.call( {"entitlement": {"entitled": True, "type": "ent1"}}, { "entitlement": { "entitled": True, "type": "ent1", "new": "newval", } }, allow_enable=False, ), mock.call( {"entitlement": {"entitled": False, "type": "ent2"}}, {"entitlement": {"entitled": False, "type": "ent2"}}, allow_enable=False, ), ] assert process_calls == process_entitlement_delta.call_args_list
def action_refresh(args, cfg): if contract.request_updated_contract(cfg): print(ua_status.MESSAGE_REFRESH_SUCCESS) logging.debug(ua_status.MESSAGE_REFRESH_SUCCESS) return 0 logging.error(ua_status.MESSAGE_REFRESH_FAILURE) return 1
def test_user_facing_error_on_machine_token_refresh_failure( self, client, get_machine_id, FakeConfig): """When attaching, error on failure to refresh the machine token.""" def fake_contract_client(cfg): fake_client = FakeContractClient(cfg) fake_client._responses = { self.refresh_route: exceptions.UserFacingError("Machine token refresh fail") } return fake_client client.side_effect = fake_contract_client cfg = FakeConfig.for_attached_machine() with pytest.raises(exceptions.UserFacingError) as exc: request_updated_contract(cfg) assert "Machine token refresh fail" == str(exc.value)
def test_user_facing_error_due_to_unexpected_process_entitlement_delta( self, client, get_machine_id, process_entitlement_delta, first_error, second_error, ux_error_msg, FakeConfig, ): """Unexpected errors from process_entitlement_delta are raised. Remaining entitlements are processed regardless of error and error is raised at the end. Unexpected exceptions take priority over the handled UserFacingErrors. """ # Fail first and succeed second call to process_entitlement_delta process_entitlement_delta.side_effect = ( first_error, second_error, None, ) # resourceEntitlements specifically ordered reverse alphabetically # to ensure proper sorting for process_contract_delta calls below machine_token = { "machineToken": "mToken", "machineTokenInfo": { "contractInfo": { "id": "cid", "resourceEntitlements": [ {"entitled": False, "type": "ent3"}, {"entitled": False, "type": "ent2"}, {"entitled": True, "type": "ent1"}, ], } }, } cfg = FakeConfig.for_attached_machine(machine_token=machine_token) fake_client = FakeContractClient(cfg) fake_client._responses = { self.refresh_route: machine_token, self.access_route_ent1: { "entitlement": { "entitled": True, "type": "ent1", "new": "newval", } }, } client.return_value = fake_client with pytest.raises(exceptions.UserFacingError) as exc: assert None is request_updated_contract(cfg) assert 3 == process_entitlement_delta.call_count assert ux_error_msg == str(exc.value)
def action_enable(args, cfg): """Perform the enable action on a named entitlement. @return: 0 on success, 1 otherwise """ logging.debug(ua_status.MESSAGE_REFRESH_ENABLE) if not contract.request_updated_contract(cfg): logging.debug(ua_status.MESSAGE_REFRESH_FAILURE) return 0 if _perform_enable(args.name, cfg) else 1
def test_user_facing_error_on_service_token_refresh_failure( self, client, get_machine_id): """When attaching, error on any failed specific service refresh.""" machine_token = { "machineToken": "mToken", "machineTokenInfo": { "contractInfo": { "id": "cid", "resourceEntitlements": [ { "entitled": True, "type": "ent2" }, { "entitled": True, "type": "ent1" }, ], } }, } def fake_contract_client(cfg): fake_client = FakeContractClient(cfg) fake_client._responses = { self.refresh_route: machine_token, self.access_route_ent1: exceptions.UserFacingError("Broken ent1 route"), self.access_route_ent2: exceptions.UserFacingError("Broken ent2 route"), } return fake_client client.side_effect = fake_contract_client cfg = FakeConfig.for_attached_machine(machine_token=machine_token) with pytest.raises(exceptions.UserFacingError) as exc: request_updated_contract(cfg) assert "Broken ent1 route" == str(exc.value)
def test_attached_config_refresh_machine_token_and_services( self, client, get_machine_id, process_entitlement_delta): """When attached, refresh machine token and entitled services. Processing service deltas are processed in a sorted order based on name to ensure operations occur the same regardless of dict ordering. """ refresh_route = API_V1_TMPL_CONTEXT_MACHINE_TOKEN_REFRESH.format( contract='cid', machine='mid') access_route_ent1 = API_V1_TMPL_RESOURCE_MACHINE_ACCESS.format( resource='ent1', machine='mid') # resourceEntitlements specifically ordered reverse alphabetically # to ensure proper sorting for process_contract_delta calls below machine_token = { 'machineToken': 'mToken', 'machineTokenInfo': {'contractInfo': { 'id': 'cid', 'resourceEntitlements': [ {'entitled': False, 'type': 'ent2'}, {'entitled': True, 'type': 'ent1'}]}}} def fake_contract_client(cfg): client = FakeContractClient(cfg) # Note ent2 access route is not called client._responses = { refresh_route: machine_token, access_route_ent1: { 'entitlement': { 'entitled': True, 'type': 'ent1', 'new': 'newval'}}} return client client.side_effect = fake_contract_client cfg = FakeConfig.for_attached_machine(machine_token=machine_token) assert True is request_updated_contract(cfg) assert machine_token == cfg.read_cache('machine-token') # Redact public content assert ( '<REDACTED>' == cfg.read_cache( 'public-machine-token')['machineToken']) # Deltas are processed in a sorted fashion so that if enableByDefault # is true, the order of enablement operations is the same regardless # of dict key ordering. process_calls = [ mock.call({'entitlement': {'entitled': True, 'type': 'ent1'}}, {'entitlement': {'entitled': True, 'type': 'ent1', 'new': 'newval'}}, allow_enable=False), mock.call({'entitlement': {'entitled': False, 'type': 'ent2'}}, {'entitlement': {'entitled': False, 'type': 'ent2'}}, allow_enable=False)] assert process_calls == process_entitlement_delta.call_args_list
def action_attach(args, cfg): if cfg.is_attached: print("This machine is already attached to '%s'." % cfg.accounts[0]['name']) return 0 if os.getuid() != 0: print(ua_status.MESSAGE_NONROOT_USER) return 1 contract_client = contract.UAContractClient(cfg) if not args.token: bound_macaroon_bytes = sso.discharge_root_macaroon(contract_client) if bound_macaroon_bytes is None: print('Could not attach machine. Unable to obtain authenticated' ' user token') return 1 bound_macaroon = bound_macaroon_bytes.decode('utf-8') cfg.write_cache('bound-macaroon', bound_macaroon) try: contract_client.request_accounts(macaroon_token=bound_macaroon) contract_token = contract.get_contract_token_for_account( contract_client, bound_macaroon, cfg.accounts[0]['id']) except (sso.SSOAuthError, util.UrlError) as e: logging.error(str(e)) print('Could not attach machine. Unable to obtain authenticated' ' contract token') return 1 else: contract_token = args.token if not contract_token: print('No valid contract token available') return 1 if not contract.request_updated_contract( cfg, contract_token, allow_enable=True): print( ua_status.MESSAGE_ATTACH_FAILURE_TMPL.format(url=cfg.contract_url)) return 1 contract_name = ( cfg.machine_token['machineTokenInfo']['contractInfo']['name']) print( ua_status.MESSAGE_ATTACH_SUCCESS_TMPL.format( contract_name=contract_name)) action_status(args=None, cfg=cfg) return 0
def action_refresh(args, cfg): if contract.request_updated_contract(cfg): print(ua_status.MESSAGE_REFRESH_SUCCESS) logging.debug(ua_status.MESSAGE_REFRESH_SUCCESS) return 0 raise exceptions.UserFacingError(ua_status.MESSAGE_REFRESH_FAILURE)
def can_enable(self) -> Tuple[bool, Optional[CanEnableFailure]]: """ Report whether or not enabling is possible for the entitlement. :return: (True, None) if can enable (False, CanEnableFailure) if can't enable """ if self.is_access_expired(): logging.debug( "Updating contract on service '%s' expiry", self.name ) contract.request_updated_contract(self.cfg) if not self.contract_status() == ContractStatus.ENTITLED: return ( False, CanEnableFailure( CanEnableFailureReason.NOT_ENTITLED, message=messages.UNENTITLED.format(title=self.title), ), ) application_status, _ = self.application_status() if application_status != ApplicationStatus.DISABLED: return ( False, CanEnableFailure( CanEnableFailureReason.ALREADY_ENABLED, message=messages.ALREADY_ENABLED.format(title=self.title), ), ) if not self.valid_service: return (False, CanEnableFailure(CanEnableFailureReason.IS_BETA)) applicability_status, details = self.applicability_status() if applicability_status == ApplicabilityStatus.INAPPLICABLE: return ( False, CanEnableFailure( CanEnableFailureReason.INAPPLICABLE, message=details ), ) if self.incompatible_services: if self.detect_incompatible_services(): return ( False, CanEnableFailure( CanEnableFailureReason.INCOMPATIBLE_SERVICE ), ) if self.required_services: if not self.check_required_services_active(): return ( False, CanEnableFailure( CanEnableFailureReason.INACTIVE_REQUIRED_SERVICES ), ) return (True, None)