Update shipping UPS API to OAuth

UPS has implemented an OAuth 2.0 security model for all APIs and deprecate the
existing Access Key-based authorisation.
See https://developer.ups.com/oauth-developer-guide
This commit is contained in:
Cédric Krier committed 2023-06-01 16:42:06 +02:00
1 parent 7e7fd51dd1
commit ed30d063e7
8 files changed
+59 -51

No files matched your search

@@ -1,3 +1,4 @@
* Update to OAuth
Version 6.8.0 - 2023-05-01
--------------------------
+35 -3
View File
@@ -1,5 +1,9 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from oauthlib.oauth2 import BackendApplicationClient
from requests_oauthlib import OAuth2Session
from trytond.model import (
MatchMixin, ModelSQL, ModelView, fields, sequence_ordered)
from trytond.pool import PoolMeta
@@ -9,11 +13,11 @@ from trytond.pyson import Eval
class CredentialUPS(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
'UPS Credential'
__name__ = 'carrier.credential.ups'
_rec_name = 'account_number'
company = fields.Many2One('company.company', 'Company')
user_id = fields.Char('User ID', required=True)
password = fields.Char('Password', required=True)
license = fields.Char('License', required=True)
client_id = fields.Char("Client ID", required=True)
client_secret = fields.Char("Client Secret", required=True)
account_number = fields.Char('Account Number', required=True)
use_metric = fields.Boolean('Use Metric')
server = fields.Selection([
@@ -21,6 +25,16 @@ class CredentialUPS(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
('production', 'Production'),
], 'Server')
@classmethod
def __register__(cls, module):
table_h = cls.__table_handler__(module)
super().__register__(module)
# Migration from 6.8: switch to OAuth
table_h.drop_column('user_id')
table_h.drop_column('password')
table_h.drop_column('license')
@classmethod
def default_use_metric(cls):
return True
@@ -29,6 +43,24 @@ class CredentialUPS(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
def default_server(cls):
return 'testing'
def get_token(self):
if self.server == 'production':
url = 'https://onlinetools.ups.com/security/v1/oauth/token'
else:
url = 'https://wwwcie.ups.com/security/v1/oauth/token'
client = BackendApplicationClient(client_id=self.client_id)
oauth = OAuth2Session(client=client)
token = oauth.fetch_token(
token_url=url, client_id=self.client_id,
client_secret=self.client_secret)
return token['access_token']
def get_shipment_url(self):
if self.server == 'production':
return 'https://onlinetools.ups.com/api/shipments/v1/ship'
else:
return 'https://wwwcie.ups.com/api/shipments/v1/ship'
class Carrier(metaclass=PoolMeta):
__name__ = 'carrier'
@@ -2,9 +2,9 @@ Stock Package Shipping UPS Module
#################################
The Stock Package Shipping UPS module allows you to generate the UPS labels per
package using the UPS webservices.
package using the UPS API.
The credential for the webservice is specified by the following fields:
The credential for the API is specified by the following fields:
UPS Carrier Credential
**********************
@@ -14,15 +14,13 @@ The credentials used to connect to the UPS API will be automatically retrieved
based on the company of the Shipment.
- *Company*: The company for which those credentials are valid
- *User ID*: The User ID provided by UPS.
- *Password*: The Password used to access the MyUPS Portal.
- *Client ID*: The Client ID of the App.
- *Client Secret*: The Client Secret of the App.
- *Account Number*: The account number UPS assigned to you when you requested the
API credentials (also known as the Shipper Number).
- *License*: The License number UPS assigned to you when you requested the API
credentials
- *Server*: Are those credentials used for accessing the Testing or the
Production server of UPS.
- *Use Metric*: Use Metric units when communicating with the UPS webservice.
- *Use Metric*: Use Metric units when communicating with the UPS API.
Carrier
*******
+1 -1
View File
@@ -41,7 +41,7 @@ else:
download_url = 'http://downloads.tryton.org/%s.%s/' % (
major_version, minor_version)
requires = ['requests']
requires = ['requests', 'requests-oauthlib', 'oauthlib']
for dep in info.get('depends', []):
if not re.match(r'(ir|res)(\W|$)', dep):
requires.append(get_require_version('trytond_%s' % dep))
+8 -28
View File
@@ -21,10 +21,6 @@ from trytond.wizard import StateAction, StateTransition, Wizard
from .exceptions import UPSError
SERVER_URLS = {
'testing': 'https://wwwcie.ups.com/json/Ship',
'production': 'https://onlinetools.ups.com/json/Ship',
}
TRACKING_URL = 'https://www.ups.com/track'
TIMEOUT = config.getfloat(
'stock_package_shipping_ups', 'requests_timeout', default=300)
@@ -178,15 +174,20 @@ class CreateShippingUPS(Wizard):
carrier = shipment.carrier
packages = shipment.root_packages
shipment_request = self.get_request(shipment, packages, credential)
api_url = config.get('stock_package_shipping_ups', credential.server,
default=SERVER_URLS[credential.server])
token = credential.get_token()
api_url = credential.get_shipment_url()
headers = {
'transactionSrc': "Tryton",
'Authorization': f"Bearer {token}",
}
nb_tries, response = 0, None
error_message = ''
try:
while nb_tries < 5 and response is None:
try:
req = requests.post(
api_url, json=shipment_request, timeout=TIMEOUT)
api_url, json=shipment_request, headers=headers,
timeout=TIMEOUT)
except ssl.SSLError as e:
error_message = e.reason
nb_tries += 1
@@ -201,15 +202,6 @@ class CreateShippingUPS(Wizard):
gettext('stock_package_shipping_ups.msg_ups_webservice_error',
message=error_message))
if 'Fault' in response:
error = response['Fault']['detail']['Errors']
message = '%s\n\n%s - %s' % (response['Fault']['faultstring'],
error['ErrorDetail']['PrimaryErrorCode']['Code'],
error['ErrorDetail']['PrimaryErrorCode']['Description'])
raise UPSError(
gettext('stock_package_shipping_ups.msg_ups_webservice_error',
message=message))
shipment_response = response['ShipmentResponse']
response_status = shipment_response['Response']['ResponseStatus']
if response_status['Code'] != '1':
@@ -251,17 +243,6 @@ class CreateShippingUPS(Wizard):
if credential.match(credential_pattern):
return credential
def get_security(self, credential):
return {
'UsernameToken': {
'Username': credential.user_id,
'Password': credential.password,
},
'ServiceAccessToken': {
'AccessLicenseNumber': credential.license,
},
}
def get_request_container(self, shipment):
return {
'RequestOption': 'validate',
@@ -396,7 +377,6 @@ class CreateShippingUPS(Wizard):
for pkg in packages:
pkg['ShipmentServiceOptions'] = options
return {
'UPSSecurity': self.get_security(credential),
'ShipmentRequest': {
'Request': self.get_request_container(shipment),
'Shipment': {
@@ -155,9 +155,8 @@ Create a UPS Carrier and the related credential::
>>> credential = UPSCredential()
>>> credential.company = company
>>> credential.user_id = os.getenv('UPS_USER_ID')
>>> credential.password = os.getenv('UPS_PASSWORD')
>>> credential.license = os.getenv('UPS_LICENSE')
>>> credential.client_id = os.getenv('UPS_CLIENT_ID')
>>> credential.client_secret = os.getenv('UPS_CLIENT_SECRET')
>>> credential.account_number = os.getenv('UPS_ACCOUNT_NUMBER')
>>> credential.server = 'testing'
>>> credential.save()
@@ -7,15 +7,13 @@ this repository contains the full copyright notices and license terms. -->
<label name="sequence"/>
<field name="sequence"/>
<label name="account_number"/>
<field name="account_number"/>
<label name="server"/>
<field name="server"/>
<separator string="Credential Information" colspan="4" id="credential_info"/>
<label name="user_id"/>
<field name="user_id"/>
<label name="password"/>
<field name="password" widget="password"/>
<label name="license"/>
<field name="license"/>
<label name="account_number"/>
<field name="account_number"/>
<label name="client_id"/>
<field name="client_id" widget="password"/>
<label name="client_secret"/>
<field name="client_secret" widget="password"/>
</form>
@@ -3,6 +3,6 @@
this repository contains the full copyright notices and license terms. -->
<tree sequence="sequence">
<field name="company" expand="1"/>
<field name="user_id" expand="2"/>
<field name="account_number" expand="2"/>
<field name="server"/>
</tree>