Description
When attempting to make calls to a Managed Amazon Airflow instance I was not able to make API calls to the airflow environment despite being able to generate a CLI token from aws mwaa create-cli-token. Why am I getting a FORBIDDEN error here?
Console and Code
Code
#pytest.fixture(scope="function")
def run_dag(
environment_name: str,
airflow_conn_info: AirflowConnectionInfo,
dag_to_run: str,
target_date: datetime) -> Callable:
headers = {
'accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': airflow_conn_info.cli_token,
}
trigger_body = {
"conf": {},
"execution_date": target_date.isoformat(),
}
if not dag_to_run:
dag_to_run = f"{carrier_name}_dag"
base_url = f"https://{airflow_conn_info.hostname}/api/v1/dags/{dag_to_run}"
trigger_dag_url = f"{base_url}/dagRuns"
# TODO: Add some sort of check to ensure a DAG isn't disabled OR add a call
# to enable the DAG. For now we're just going to assume it's enabled.
trigger_response = requests.post(
trigger_dag_url,
headers=headers,
data=json.dumps(trigger_body))
if trigger_response.status_code != 200:
raise ValueError(f"{trigger_response}")
dag_run_status_url = f"{base_url}/{trigger_response.json()['dag_id']}"
status_body = {}
task_instances_status_url = f"{base_url}/dagRuns/{trigger_response.json()['dag_id']/taskInstances}"
task_instances_body = {}
status_response = requests.get(
dag_run_status_url,
headers=headers,
data=json.dumps(status_body))
if status_response.status_code != 200:
raise ValueError(f"{trigger_response}")
# Derived from
# https://github.com/apache/airflow/blob/main/airflow/utils/state.py
terminal_states: List[str] = ["success", "failed"]
# TODO: Add a timeout to this while loop.
while (trigger_response.status_code == 200
and trigger_response.json()["state"] not in terminal_states):
# TODO: Add some sort of console output to show what task instance we're
# on and the state of that current task instance.
status_response = requests.get(
dag_run_status_url,
headers=headers,
data=json.dumps(status_body))
task_instances_response = requests.get(
task_instances_status_url,
headers=headers,
data=json.dumps(task_instances_body))
breakpoint()
Failing Run PDB
(Pdb) base_url
'https://{a_string}.c46.us-east-1.airflow.amazonaws.com/api/v1/dags/fedex_dag'
(Pdb) trigger_response.json()
{'detail': None, 'status': 401, 'title': 'Unauthorized', 'type': 'https://airflow.apache.org/docs/2.0.2/stable-rest-api-ref.html#section/Errors/Unauthenticated'}
(Pdb) headers
{'accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': '{secret}'}
TLDR: The API is turned off by default. This is not explicitly stated anywhere. You should use the https://YOUR_HOST_NAME/aws_mwaa/cli endpoint unless you want to enable the full API.
When I was reading the AWS documentation around generating a CLI token what was not clear to me is that the aws_mwaa/cli endpoint that AWS adds to the MWAA is the endpoint that they want you to use. This is explained in the user guide but not in any of the website documentation making it very unclear to me.
There is a AmazonMWAAFullApiAccess which sounds like it grants access to the full API if you have access to a role with the policy but i have not tested this yet.
Related
I have to trigger a pipeline in Azure DevOps from a python script. I have already found out that i need a private access token and that part is fine. I can, however, not get the script to work. I am trying something like this:
data = [
{
}
]
http = urllib3.PoolManager()
r = http.request('POST', api_url, headers={'Content-Type': 'application/json-patch+json', "Authorization": private_access_token}, body=data)
print(r.status)
print(r.data)
Its a requirement that i have to use urllib3 because I cant use the requests package
data is empty, because looking at the parameters here https://learn.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs/run%20pipeline?view=azure-devops-rest-6.0. Then i dont need any input data? I just want to trigger a pipeline, nothing else
Error message is not very helpful. I get error message 203.
I solved it by using:
authorization = str(base64.b64encode(bytes(':'+private_access_token, 'ascii')), 'ascii')
data = {}
a = json.dumps(data)
http = urllib3.PoolManager()
r = http.request('POST', api_url, headers = {'Content-Type': 'application/json', 'Authorization': 'Basic '+authorization}, body=a)
Xero has changed its API to require OAuth2 connections instead of OAuth1.
I had a working solution in OAuth1, but the examples for Oauth2 are scarce at best, but mainly for web sites.
I'm another developer who managed to create an Oauth1 solution, that successfully worked as a machine to machine solution with no web server involved.
Xero has some examples that run in Postman, that easily work in my test environment.
I'm attempting to reproduce the postman action of refreshing the token in Python3.
My basic code below is where I currently am:
#client_id = "xxxxx"
#client_secret = "xxxxx"
callback_url = "https://api.xero.com/connections"
re_directURI = "https://developer.xero.com"
scopes = "offline_access accounting.contacts accounting.transactions"
refresh_url = "https://identity.xero.com/connect/token"
access_token = open('AccessToken.txt').read()
old_refresh_token = open('RefreshToken.txt','r').read()
# Refresh Token code...
import requests
#def refresh_xero_token(refresh_token):
headers = {
'grant_type': 'refresh_token',
'Content-Type': 'application/json',
}
data = {
'grant_type': 'refresh_token',
'refresh_token': old_refresh_token,
'client_id': client_id,
'client_secret': client_secret
}
print(data,headers)
response = requests.post(refresh_url, headers=headers, data=data)
#return response.json()
print(response.text)
I have so far failed to find an example that works without a web server, just using python to communicate with the Xero servers to transfer local data into the Zero API.
Using xoauth,.exe (windows) to get the access_token, and then in postman I can run the examples for refresh token, connections, invoices etc to the demo company.
and I believe just being able to replicate these examples will provide me with what I need to get a working solution.
currently with this python code I only get
{"error":"invalid_request"}
So, clearly I am missing something.
I'll class myself as a newbie with Python or Oauth2, but have chosen this path due to my previous success with an Oauth1 connected solution.
I would ask the Xero developer community, but I'm writing this for a user of our software to push data into their Xero accounts, and so for testing I only have a trial account, which does not give me access to the Xero developer community.
Which by itself is really annoying.
Xero support seems of little use also, I tried.
If there is anyone out there able to assist, that would be fabulous.
Thank you in advance for any help given.
After using the xoauth application and setting up the connection, I have since found that with the refresh token, running this function keeps the connection up and running.
def refresh_xero_token():
refresh_url = "https://identity.xero.com/connect/token"
old_refresh_token = open('RefreshToken.txt','r').read()
tokenb4 = f"{client_id}:{client_secret}"
basic_token = base64.urlsafe_b64encode(tokenb4.encode()).decode()
headers = {
'Authorization': f"Basic {basic_token}",
'Content-Type': 'application/x-www-form-urlencoded',
}
data = {
'grant_type': 'refresh_token',
'refresh_token': old_refresh_token
}
try:
response = requests.post(refresh_url, headers=headers, data=data)
results = response.json()
open('RefreshToken.txt','w').write(results["refresh_token"])
open('AccessToken.txt','w').write(results["access_token"])
except Exception as err:
print("ERROR ! Refreshing token error?")
print(response.text)
As additional information, I can then also use this connection to, for example create a contact in Xero:
In this example the irContact is a SQLAlchemy row of data from a SQL table.
def create_contact( connection, irContact, access_token):
#Setup new contact
address1 = {"AddressType": "POBOX"}
if irContact['addressline1'] is not None: address1.update({"AddressLine1": irContact['addressline1']})
if irContact['addressline2'] is not None: address1.update({"AddressLine2": irContact['addressline2']})
if irContact['addressline3'] is not None: address1.update({"AddressLine3": irContact['addressline3']})
if irContact['addressline4'] is not None: address1.update({"AddressLine4": irContact['addressline4']})
if irContact['city'] is not None: address1.update({"City": irContact['city']})
if irContact['region'] is not None: address1.update({"Region": irContact['region']})
if irContact['postalcode'] is not None: address1.update({"PostalCode": irContact['postalcode']})
if irContact['country'] is not None: address1.update({"Country": irContact['country']})
if irContact['attentionto'] is not None: address1.update({"AttentionTo": irContact['attentionto']})
#print (address1.values())
addresses = []
addresses.append(address1)
phones = []
if irContact['phonenumber'] is not None:
phone1 = {"PhoneType": "DEFAULT"}
#phone1.update({"PhoneType": "DEFAULT"})
if irContact['phonenumber'] is not None: phone1.update({"PhoneNumber": irContact['phonenumber']})
if irContact['phoneareacode'] is not None: phone1.update({"PhoneAreaCode": irContact['phoneareacode']})
if irContact['phonecountrycode'] is not None: phone1.update({"PhoneCountryCode": irContact['phonecountrycode']})
phones.append(phone1)
#print (phone1.values())
if irContact['mobilenumber'] is not None:
phone2 = {"PhoneType": "MOBILE"}
if irContact['phonenumber'] is not None: phone2.update({"PhoneNumber": irContact['mobilenumber']})
if irContact['phoneareacode'] is not None: phone2.update({"PhoneAreaCode": irContact['mobileareacode']})
if irContact['phonecountrycode'] is not None: phone2.update({"PhoneCountryCode": irContact['mobilecountrycode']})
phones.append(phone2)
#print (phone2.values())
contact = { "Name": irContact['name'],
"ContactNumber": irContact['contactnumber'],
"AccountNumber": irContact['accountnumber'],
#"ContactStatus": "ACTIVE",
"FirstName": irContact['firstname'],
"LastName": irContact['lastname'],
#"EmailAddress": irContact['emailaddress'],
"Addresses": addresses,
#"Phones":phones
}
contacts = [contact]
#print(contacts)
contacts_url = "https://api.xero.com/api.xro/2.0/Contacts"
headers = {
'Authorization': f"Bearer {access_token}",
'Accept': 'application/json',
'Content-Type': 'application/json',
'xero-tenant-id': tenant_id,
}
data = {
'Contacts': [contact],
}
#print(data)
try:
response = requests.post(contacts_url, headers=headers, json=data)
except Exception as err:
print("ERROR! Contact: %s" % (str(err) ))
print(response.text)
return 0
#print(response.text)
results = response.json()
if 'Contacts' in results:
newcontacts = results['Contacts']
for newcontact in newcontacts: break
query = "update xero_contact set errortext='', ContactID='%s' where id=%d" % (newcontact["ContactID"], irContact['id'])
connection.execute(query)
query = "update xero_invoice_header set ContactID='%s' where OurContactID=%d and (InvoiceID='' or InvoiceID is null ) " % ( newcontact["ContactID"], irContact['id'] )
connection.execute(query)
I believe, with this amount of information, anyone can be capable of creating their own Xero machine to machine interface...
Realising that other records can be read and created using minimal tweaks to the header and or data element of the requests call.
I found the lack of this information so very frustrating, if people can find this, it may help them in future.
I'm fairly new to using web APIs and pulling data and i'm also pretty new with python. My goal is to make a stat-tracker app but I keep getting a 401 when I try and pull the data.
I've printed out the entire url just to make sure I didn't get it wrong. I copied and pasted the API key exactly so that shouldn't be a problem
api_token = 'api key in python file'
api_url_base = 'https://public-api.tracker.gg/v2/apex/standard/'
headers = {'Content-Type' : 'application/json',
'Authorization' : 'Bearer {}'.format(api_token)}
def get_player_profile():
api_url = '{}profile/psn/Daltoosh'.format(api_url_base)
response = requests.get(api_url, headers=headers)
if response.status_code == 200:
return json.loads(response.content.decode('utf-8'))
else:
return response.status_code, api_url
print(get_player_profile())
#player_profile = get_player_profile()
#if player_profile is not None:
# print("Career Stats:")
# for k, v in player_profile['profile/psn/Daltoosh'].items():
# print('{0}:{1}.format(k, v)')
#else:
# print('[!] Data Request Failed [!]')
I expected a status code of 200 but there seems to be a problem authenticating.
I'm not too well versed in the web API that you are using, but I think you might be using the API token incorrectly. I don't think that specific API requires a Bearer token, but instead a separate header called TRN-Api-Key.
So maybe write something like this:
headers = {'Content-Type' : 'application/json', 'TRN-Api-Key' : api_token}
If you look here, you should be able to read up on how to set up authentication.
I am able to create a simple API interface using the requests module that authenticates correctly and receives a response from an API. However, when I attempt to use bravado, to create the client from a swagger file, and manually add an authorization token to the head, it fails with :
bravado.exception.HTTPUnauthorized: 401 Unauthorized: Error(code=u'invalid_credentials', message=u'Missing authorization header',
I believe I am adding the authorization headers correctly.
The code I'm using to create the client is below. As shown, I've tried to add an Authorization token two ways:
in the http_client setup via set_api_key
in the Swagger.from_url(...) step by adding request_headers.
However both options fail.
from bravado.requests_client import RequestsClient
from bravado.client import SwaggerClient
http_client = RequestsClient()
http_client.set_api_key(
'https://api.optimizely.com/v2', 'Bearer <TOKEN>',
param_name='Authorization', param_in='header'
)
headers = {
'Authorization': 'Bearer <TOKEN>',
}
client = SwaggerClient.from_url(
'https://api.optimizely.com/v2/swagger.json',
http_client=http_client,
request_headers=headers
)
My question is, how do I properly add authorization headers to a bravado SwaggerClient?
For reference, a possible solution is to add the _request_options with each request:
from bravado.client import SwaggerClient
headers = {
'Authorization': 'Bearer <YOUR_TOKEN>'
}
requestOptions = {
# === bravado config ===
'headers': headers,
}
client = SwaggerClient.from_url("<SWAGGER_JSON_URL>")
result = client.<ENTITY>.<ACTION>(_request_options=requestOptions).response().result
print(result)
However, a better solution, which I still am unable to get to work, is to have it automatically authenticate with each request.
Try again, fixing the host of the set_api_key line.
from bravado.requests_client import RequestsClient
from bravado.client import SwaggerClient
http_client = RequestsClient()
http_client.set_api_key(
'api.optimizely.com', 'Bearer <TOKEN>',
param_name='api_key', param_in='header'
)
client = SwaggerClient.from_url(
'https://api.optimizely.com/v2/swagger.json',
http_client=http_client,
)
Here you will find documentation about the method : https://github.com/Yelp/bravado/blob/master/README.rst#example-with-header-authentication
I am trying to integrate qwikcilver API in my project. They are using oauth1.0 for authentication. I am using requests-oauthlib python lib for oauth1.0.
Here is my code for authentication.
# Using OAuth1Session
oauth = OAuth1Session(client_key, client_secret=client_secret)
fetch_response = oauth.fetch_request_token(request_token_url)
{
"oauth_token": "Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik",
"oauth_token_secret": "Kd75W4OQfb2oJTV0vzGzeXftVAwgMnEK9MumzYcM"
}
resource_owner_key = fetch_response.get('oauth_token')
resource_owner_secret = fetch_response.get('oauth_token_secret')
My query is as following,
qwikcilver has username and password. I tried lot to send username and password in code but not working.
How to pass username and password in requests-oauthlib request function?
Check with qwikcilver (whatever that is) if they have provision to generate token and authorize. If so, you can use the token as a part of the header and invoke the APIs. If that is not there, check if they allow 2-legged calls. Such services usually do not allow 2-legged calls for general users however.
For a 3-legged call, you may need a browser to complete Authorization.
In your code, you have invoked Request Token and you are trying to use it to gain access to service. Which will not work. Request Token is just a provisional token which has to be Authorized. Request Tokens cannot be used for such API calls. It will not work. After Authorization, you will need to invoke Access Token API to get your actual token - which can be used to gain access to the services you are authorized to.
In short, this is the process
1) Request Token >>
2) Authorize >>
3) Access Token
This is the flow. A sample in Python
oauth1Session = OAuth1Session(clientKey, clientSecret)
def requestToken(self):
requestTokenResponse = oauth1Session.fetch_request_token(oauth1RequestTokenUrl, realm)
token = requestTokenResponse.get('oauth_token')
secret = requestTokenResponse.get('oauth_token_secret')
return (token, secret)
def authorize(self, token):
authUrl = oauth1Session.authorization_url(oauth1AuthorizeUrl, token)
print (authUrl)
#########
# THIS IS WHERE YOU NEED THE BROWSER.
# You visit authUrl and login with your Username and Password.
# This will complete Authorization
return authUrl
def accessToken(self):
accessTokenResponse = oauth1Session.fetch_access_token(oauth1AccessTokenUrl)
print (accessTokenResponse)
#########
#accessTokenResponse contains your actual token
#
For the browser part - you can try Chromium bindings for Python (there are few who have tried it before - for example this one https://github.com/cztomczak/cefpython). There are other options such as using your default installed browser and other such. Choose whatever works for you.
Once you have that in place - you may programatically visit the URL (authUrl) and once authorized (login, then 'allow') - you may be redirected to a callback (which you specified in case of OAuth1) with the "code" query string. This is the Authorization Code.
Once you have the authorization code, you may close the browser window and invoke the Get Access Token call (fetch_access_token).
Hope this helps !
Im adding an example with a post body. This is a simple example using a request/post with a "Plain Old XML" (POX) pattern:
from requests_oauthlib import OAuth1Session
CONSUMER_KEY = "xxxxxxx"
CONSUMER_SECRET = "xxxxxxx"
ourSession = OAuth1Session(CONSUMER_KEY, client_secret=CONSUMER_SECRET, force_include_body=True)
body= '<?xml version="1.0" encoding="UTF-8"?>' \
'<POXEnvelopeRequest xmlns="http://whateve">' \
'<POXHeader>' \
' <RequestHeaderInfo>' \
. . . .
' </RequestHeaderInfo>' \
'</POXHeader>' \
'</POXEnvelopeRequest>'
clen = str(len(body))
headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Host': 'X.Y.com',
'Content-Type': 'application/xml',
'Connection': 'keep-alive',
'Content-Length': clen
}
r = ourSession.post(url, headers=headers, data=body, verify=False)
# DEBUG: Comment out in and out as needed...
print("===================== B E G I N R E S P O N S E =======================\n")
print(r)
print(r.text)
print("===================== E N D of R E S P O N S E =======================\n")