Update Org Policy constraints with Python - python

I am working on a small project, to update an org policy constraints by using python.
I want to use python because I have set up Secret Manager and Impersonation.
Right now I am at this final stage, of modifying the org policy constraint
I have found the repo https://github.com/googleapis/python-org-policy/tree/40faa07298b3baa9a4d0ca26927b28fdd80aa03b/samples/generated_samples
With a code sample for creating a constraint.
I would like to modify this: "projects/project-id-from-gcp/policies/compute.skipDefaultNetworkCreation" to Enforced.
The code I have so far, is this:
from google.cloud import orgpolicy_v2
def sample_update_policy():
# Create a client
client = orgpolicy_v2.OrgPolicyClient()
# Initialize request argument(s)
request = orgpolicy_v2.UpdatePolicyRequest(
policy="""
name: "projects/project-id-from-gcp/policies/compute.skipDefaultNetworkCreation"
spec {
rules {
enforce: true
}
}
"""
)
# Make the request
response = client.update_policy(request=request)
#
# Handle the response
print(response)
sample_update_policy()
But I get the error google.api_core.exceptions.InvalidArgument: 400 Request contains an invalid argument.
I do not understand what to write exactly in "CreatePolicyRequest".
I also found this, https://googleapis.dev/python/orgpolicy/1.0.2/orgpolicy_v2/types.html#google.cloud.orgpolicy_v2.types.Policy but it is not exactly clear to me.
I was looking at this https://cloud.google.com/python/docs/reference/orgpolicy/latest/google.cloud.orgpolicy_v2.services.org_policy.OrgPolicyClient#google_cloud_orgpolicy_v2_services_org_policy_OrgPolicyClient_update_policy
But i honestly do not understand how to do it.
(I do not think what I modified it is even correct. )
Could you, please, point me in the right direction?
Thank you

Your problem is that you are passing a YAML string as the parameter to UpdatePolicyRequest(). You were on the correct path with your links.
from google.cloud import orgpolicy_v2
from google.cloud.orgpolicy_v2 import types
def build_policy():
rule = types.PolicySpec.PolicyRule()
rule.enforce = True
spec = types.PolicySpec()
spec.rules.append(rule)
policy = types.Policy(
name="projects/project-id-from-gcp/policies/compute.skipDefaultNetworkCreation",
spec = spec
)
return policy
def sample_update_policy():
# Create a client
client = orgpolicy_v2.OrgPolicyClient()
policy = build_policy()
# Debug - view created policy
print(policy)
# Initialize request argument(s)
request = orgpolicy_v2.UpdatePolicyRequest(
policy=policy
)
# Make the request
response = client.update_policy(request=request)
#
# Handle the response
print(response)
sample_update_policy()

Related

Automatically subscribe to YouTube channels via Channel_ID

I'm fairly new to Python and I'm trying to migrate subscriptions from an older youtube account to a newer one that I'll use going forward. I pulled my subscriptions export from the old one and have around 470+ subs that I'll need to migrate over.
I found this article which absolutely works with automatically subscribing to a youtube channel via their channel_id but it seems like in the key value pair I can only run the .py script once per value.
I tried all sorts of googling to see how I can include multiple values in the key (channelId) but it always only auto subs to the last one in the dictionary.
Can someone please help show me what I'm missing? I feel like there has to be a way to add multiple channelId values in there key dictionary, right?!
Here's what my code looks like > screenshot
import os
import google.oauth2.credentials
import google_auth_oauthlib.flow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from google_auth_oauthlib.flow import InstalledAppFlow
# The CLIENT_SECRETS_FILE variable specifies
# the name of a file that contains
# client_id and client_secret.
CLIENT_SECRETS_FILE = "client_secret.json"
# This scope allows for full read/write access
# to the authenticated user's account and
# requires requests to use an SSL connection.
SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
def get_authenticated_service():
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS_FILE, SCOPES)
credentials = flow.run_console()
return build(API_SERVICE_NAME, API_VERSION, credentials = credentials)
def print_response(response):
print(response)
# Build a resource based on a list of
# properties given as key-value pairs.
# Leave properties with empty values out
# of the inserted resource.
def build_resource(properties):
resource = {}
for p in properties:
# Given a key like "snippet.title", split into
# "snippet" and "title", where "snippet" will be
# an object and "title" will be a property in that object.
prop_array = p.split('.')
ref = resource
for pa in range(0, len(prop_array)):
is_array = False
key = prop_array[pa]
# For properties that have array values, convert a name like
# "snippet.tags[]" to snippet.tags, and set a flag to handle
# the value as an array.
if key[-2:] == '[]':
key = key[0:len(key)-2:]
is_array = True
if pa == (len(prop_array) - 1):
# Leave properties without values
# out of inserted resource.
if properties[p]:
if is_array:
ref[key] = properties[p].split(', ')
else:
ref[key] = properties[p]
elif key not in ref:
# For example, the property is "snippet.title",
# but the resource does not yet have a "snippet"
# object. Create the snippet object here.
# Setting "ref = ref[key]" means that in the
# next time through the "for pa in range ..." loop,
# we will be setting a property in the
# resource's "snippet" object.
ref[key] = {}
ref = ref[key]
else:
# For example, the property is "snippet.description",
# and the resource already has a "snippet" object.
ref = ref[key]
return resource
# Remove keyword arguments that are not set
def remove_empty_kwargs(**kwargs):
good_kwargs = {}
if kwargs is not None:
for key, value in kwargs.items():
if value:
good_kwargs[key] = value
return good_kwargs
def subscriptions_insert(client, properties, **kwargs):
resource = build_resource(properties)
kwargs = remove_empty_kwargs(**kwargs)
response = client.subscriptions().insert(
body = resource,**kwargs).execute()
return print_response(response)
if __name__ == '__main__':
# When running locally, disable OAuthlib's
# HTTPs verification. When running in production
# * do not * leave this option enabled.
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
client = get_authenticated_service()
subscriptions_insert(client,
{'snippet.resourceId.kind': 'youtube# channel',
'snippet.resourceId.channelId': 'UC09fL42MpkktKZWmWxYiDhw', 'UC0Q7Hlz75NYhYAuq6O0fqHw'},
part ='snippet')```
According to YouTube Data API v3 documentation (Subscriptions: insert endpoint and Subscriptions resource), it seems that you can only subscribe a channel at a time. As you have by default 10,000 of quota per day, except if you request extended quota, because Subscriptions: insert costs 50 of quota, then for 470+ subscriptions, you would need 3 days to proceed.
Otherwise you can proceed as follows, it seems that the first time I tried with ~500 channels I have been subscribed to ~290 of them but now I mostly only receive (when removing -H 'Accept-Encoding: gzip, deflate, br' from the cURL request):
{
"error": {
"code": 429,
"message": "Resource has been exhausted (e.g. check quota).",
"errors": [
{
"message": "Resource has been exhausted (e.g. check quota).",
"domain": "global",
"reason": "rateLimitExceeded"
}
],
"status": "RESOURCE_EXHAUSTED"
}
}
So it's an unsure method that you can try to deepen.
Ever wondered how to do that in a single request without using any quota?
Go on an ad hoc YouTube channel YOUR_CHANNEL that you want to subscribed to: https://www.youtube.com/channel/YOUR_CHANNEL_ID
Open the Network tab of your web-browser by using Ctrl + Shift + E (on Firefox) and filter XHR requests.
Now click on Subscribe.
You should see a request to subscribe, copy it as cURL (by right-clicking).
Change at the end
"channelIds":["YOUR_CHANNEL_ID"]
to:
"channelIds":["YOUR_CHANNEL_ID_0, YOUR_CHANNEL_ID_1, ..., YOUR_CHANNEL_ID_499"]
Where YOUR_CHANNEL_ID_0 is your YOUR_CHANNEL_ID and YOUR_CHANNEL_ID_1 the second channel you want to subscribe to and so forth.
Execute the modified cURL request in a terminal and that's it!
Note that this webpage contains a subscriptions count and this one contains all your subscriptions.
To get more than 249 different channels, I used:
import requests, json
channelIds = set()
pageToken = ''
API_KEY = 'AIzaSy...'
i = 0
while len(channelIds) < 250:
url = f'https://www.googleapis.com/youtube/v3/search?q={i}&type=channel&maxResults=50&key={API_KEY}'
if pageToken != '':
url += f"&pageToken={pageToken}"
content = requests.get(url).text
data = json.loads(content)
for item in data['items']:
channelIds.add(item['id']['channelId'])
print(len(channelIds))
if 'nextPageToken' in data:
pageToken = data['nextPageToken']
else:
break
i += 1
print('["' + '","'.join(channelIds) + '"]', len(channelIds))
As #Benjamin Loison has mentioned, there is a quota on the limit on the usage of the API. If you'd like to raise the limit, I think there is a form you can fill out to request more. However, I don't recommend you do so since the form is applicable mainly for a large application that will be used for a long time and involve a long process of human inspection on what you're trying to build (This is based on my personal experience, might not be entirely accurate).
My suggestion would be to use the script you have to print out a list of channel links, and you can click into each of them and press the subscribe button. 470-ish channels should not take you a long time.

How to add a new item using Python Etsy HTTP API methods?

I am trying to use Etsy API to add a new listing on my store. In the documents section it says (below section how to do it). First fyi I have never used HTTP Method before so I am not sure how to setup the code so that it adds a new item.
(Link to the Etsy API page https://www.etsy.com/developers/documentation/reference/listing).
Method Name createListing
Synopsis Creates a new Listing.
HTTP Method POST
URI /listings
Parameters
Name Required Default Type
quantity Y int
title Y string
description Y text
price Y float
materials N array(string)
shipping_template_id N int
shop_section_id N int
image_ids N array(int)
is_customizable N boolean
non_taxable N boolean
image N image
state N active enum(active, draft)
processing_min N int
processing_max N int
category_id N int
taxonomy_id N int
tags N array(string)
who_made Y enum(i_did, collective, someone_else)
is_supply Y boolean
when_made Y enum(made_to_order, 2010_2017, 2000_2009, 1998_1999, before_1998, 1990_1997, 1980s, 1970s, 1960s, 1950s, 1940s, 1930s, 1920s, 1910s, 1900s, 1800s, 1700s, before_1700)
recipient N enum(men, women, unisex_adults, teen_boys, teen_girls, teens, boys, girls, children, baby_boys, baby_girls, babies, birds, cats, dogs, pets, not_specified)
occasion N enum(anniversary, baptism, bar_or_bat_mitzvah, birthday, canada_day, chinese_new_year, cinco_de_mayo, confirmation, christmas, day_of_the_dead, easter, eid, engagement, fathers_day, get_well, graduation, halloween, hanukkah, housewarming, kwanzaa, prom, july_4th, mothers_day, new_baby, new_years, quinceanera, retirement, st_patricks_day, sweet_16, sympathy, thanksgiving, valentines, wedding)
style N array(string)
Requires OAuth Y
Permission Scope listings_w
Notes
A shipping_template_id is required when creating a listing.
All listings created on www.etsy.com must be actual items for sale. Please see our guidelines for testingwith live listings.
Creating a listing creates a single inventory products with the supplied price and quantity. Use updateInventory to add more products.
The code I have right know looks like this
import urllib
import requests
url = 'https://openapi.etsy.com/v2/listings/active?api_key={YOUR KEY HERE)' # I put my API key here
r = requests.get(url)
payload = {'quantity': '1', 'title': 'testdfsdfdfs0','description': 'dfsdfsdfsdfdsf','price': '2.55','who_made': 'i_did','is_supply': '0','when_made': '2010_2017'}
rrr = requests.post(url,payload)
print rrr # I get an error 404
How can I add an item for sale on Etsy through Python HTTP method?
Update
from requests_oauthlib import OAuth1Session
import requests
from requests_oauthlib import OAuth1
import json
tempory_token_url = []
oauth_response_bucket = []
client_key = '.......'
client_secret = '......'
oauth = OAuth1Session(client_key, client_secret=client_secret)
request_token_url = 'https://openapi.etsy.com/v2/oauth/request_token?scope=email_r%20listings_r'
fetch_response = oauth.fetch_request_token(request_token_url)
resource_owner_key = fetch_response.get('oauth_token') # Have it
resource_owner_secret = fetch_response.get('oauth_token_secret')
oauth_url_temp = tempory_token_url[0]['login_urI']
base_authorization_url = oauth_url_temp
authorization_url = oauth.authorization_url(base_authorization_url)
redirect_response = raw_input('Paste the full redirect URL here: ')
oauth_response = oauth.parse_authorization_response(redirect_response)
verifier = oauth_response.get('oauth_verifier')
access_token_url = redeirect_response
oauth = OAuth1Session(client_key=client_secret=client_secret,resource_owner_key=resource_owner_key,resource_owner_secret=resource_owner_secret,verifier=verifier)
oauth_tokens = oauth.fetch_access_token(access_token_url)
resource_owner_key = oauth_tokens.get('oauth_token')
resource_owner_secret = oauth_tokens.get('oauth_token_secret')
Any ideas how to make this work? There is very little info regarding Etsy API and most of the stuff is in PHP which I have no clue how to work.
Image Uploading API
Everything looks the same like above this time I just changed the payload but I am getting a 403 Error. I am not sure what is causing it. My best guess would be something with oauth1.0 i think on their website it says you need oauth 1.1.
Here is how I set it up but I am getting 403 error:
url = 'https://openapi.etsy.com/v2/listings'
payload = {'listing_id':'342434342', 'image': ("test1.jpg", open('C:\\Users\\abc\\test1.jpg'),'image/jpeg'),'type':'image/jpeg'}
result = etsy.put(url, params=payload)
print result
Comment: ... at this point I am lost I have no idea where to put the pin# that etsy gave me
etsy oauth#reference
The token credentials you receive for a account do not expire,
and can be used over and over again to make authenticated API requests.
You should keep the token secret in a secure location and never send it as a plaintext parameter
(it's only used for signing your requests, and never needs to be sent in an API request on its own.)
You will not need to step through the OAuth authorization again,
unless you decides to revoke access, or unless you add features that require additional permission scopes.
Note: Didn't find a equivalent Replacement for PHP OAUTH_AUTH_TYPE_URI.
OAuth1Session Defaults to signature_type=u'AUTH_HEADER', so this could be wrong.
If this fails, you could try:
from oauthlib.oauth1 import SIGNATURE_TYPE_QUERY, SIGNATURE_TYPE_BODY
OAuth1Session(..., signature_type=SIGNATURE_TYPE_QUERY)
Create etsy OAuth1Session to reuse for Requests:
etsy = OAuth1Session(client_key,
client_secret=client_secret,
resource_owner_key=resource_owner_key,
resource_owner_secret=resource_owner_secret)
etsy Making an Authorized Request to the API:
response = etsy.get("https://openapi.etsy.com/v2/users/__SELF__")
user_data = json.loads(response.body_as_unicode())
etsy Checking Permission Scopes After Authentication:
response = etsy.get("https://openapi.etsy.com/v2/oauth/scopes")
meta = json.loads(response.body_as_unicode())
etsy Creates a new Listing
url = 'https://openapi.etsy.com/v2/listings'
payload = {'quantity': '1', 'title':...}
result = etsy.post(url, params=payload)
Comment: for api key do I need to import oauth2
According to Reference, Yes.
For write access and for accessing private user data, an OAuth access
token is required. Your application key is required to start the OAuth
authentication process.
Requires OAuth Y
Also your url should end with
URI /listings
url = 'https://openapi.etsy.com/v2/listings'
Your url should only up to the Question mark, for example:
url = 'https://openapi.etsy.com/v2/listings/active'
payload = {'api_key':YOUR KEY HERE, 'quantity': '1', ...
rrr = requests.post(url, params=payload)
Requests Quickstart: Passing Parameters In URLs
You often want to send some sort of data in the URL's query string.
If you were constructing the URL by hand,
this data would be given as key/value pairs in the URL after a question mark, e.g. \http://bin.org/get?key=val.
Requests allows you to provide these arguments as a dictionary of strings, using the params keyword argument.
Question: I am trying to upload a picture ... getting a 403 error
Your url Endpoint and payload isn't correct.
url = 'https://openapi.etsy.com/v2/listings'
payload = {'listing_id':'342434342', 'image': ("test1.jpg", open('C:\\Users\\abc\\test1.jpg'),'image/jpeg'),'type':'image/jpeg'}
Steps to do a etsy Request(uploadListingImage):
Read the Reference for your Method Name
Method Name uploadListingImage
HTTP Method POST
URI /listings/:listing_id/images
Parameters Name Required Default Type
listing_id Y int
listing_image_id N int
image N imagefile
...
Requires OAuth Y
Respect Supported Sizes Working with Images
Note: For me, it's unclear what the image Parameter is for.
And as it's NOT required makes no sense.
I assume its a Placeholder for the Parameter at Point 4 below: {'image':...
Build the URI
uri = 'https://openapi.etsy.com/v2/listings/342434342/images'
Create the Params Dict according to the above Reference
I recommend to use a listing_image_id, as this seems the only way to delete a Image afterwards.
params = {'listing_id':'342434342', 'listing_image_id': 1}
Create Multipart-Encoded File Dict
Image uploads can be performed using a POST request with the Content-Type: multipart/form-dataheader, following RFC1867
# PHP example from Reference:
# $params = array('#image' => '#'.$source_file.';type='.$mimetype);
files = {'image': ("test1.jpg", open('C:\\Users\\abc\\test1.jpg', 'rb'), 'image/jpeg')}
Do the Request, according the Reference, you have to use OAuth and POST
result = etsy.post(uri, params=params, files=files)
Please Comment if this is working for you or why not.

How to process JSON in Flask?

I have asked a few questions about this before, but still haven't solved my problem.
I am trying to allow Salesforce to remotely send commands to a Raspberry Pi via JSON (REST API). The Raspberry Pi controls the power of some RF Plugs via an RF Transmitter called a TellStick. This is all setup, and I can use Python to send these commands. All I need to do now is make the Pi accept JSON, then work out how to send the commands from Salesforce.
Someone kindly forked my repo on GitHub, and provided me with some code which should make it work. But unfortunately it still isn't working.
Here is the previous question: How to accept a JSON POST?
And here is the forked repo: https://github.com/bfagundez/RemotePiControl/blob/master/power.py
What do I need to do? I have sent test JSON messages n the Postman extension and in cURL but keep getting errors.
I just want to be able to send various variables, and let the script work the rest out.
I can currently post to a .py script I have with some URL variables, so /python.py?power=on&device=1&time=10&pass=whatever and it figures it out. Surely there's a simple way to send this in JSON?
Here is the power.py code:
# add flask here
from flask import Flask
app = Flask(__name__)
app.debug = True
# keep your code
import time
import cgi
from tellcore.telldus import TelldusCore
core = TelldusCore()
devices = core.devices()
# define a "power ON api endpoint"
#app.route("/API/v1.0/power-on/<deviceId>",methods=['POST'])
def powerOnDevice(deviceId):
payload = {}
#get the device by id somehow
device = devices[deviceId]
# get some extra parameters
# let's say how long to stay on
params = request.get_json()
try:
device.turn_on()
payload['success'] = True
return payload
except:
payload['success'] = False
# add an exception description here
return payload
# define a "power OFF api endpoint"
#app.route("/API/v1.0/power-off/<deviceId>",methods=['POST'])
def powerOffDevice(deviceId):
payload = {}
#get the device by id somehow
device = devices[deviceId]
try:
device.turn_off()
payload['success'] = True
return payload
except:
payload['success'] = False
# add an exception description here
return payload
app.run()
Your deviceID variable is a string, not an integer; it contains a '1' digit, but that's not yet an integer.
You can either convert it explicitly:
device = devices[int(deviceId)]
or tell Flask you wanted an integer parameter in the route:
#app.route("/API/v1.0/power-on/<int:deviceId>", methods=['POST'])
def powerOnDevice(deviceId):
where the int: part is a URL route converter.
Your views should return a response object, a string or a tuple instead of a dictionary (as you do now), see About Responses. If you wanted to return JSON, use the flask.json.jsonify() function:
# define a "power ON api endpoint"
#app.route("/API/v1.0/power-on/<int:deviceId>", methods=['POST'])
def powerOnDevice(deviceId):
device = devices[deviceId]
# get some extra parameters
# let's say how long to stay on
params = request.get_json()
try:
device.turn_on()
return jsonify(success=True)
except SomeSpecificException as exc:
return jsonify(success=False, exception=str(exc))
where I also altered the exception handler to handle a specific exception only; try to avoid Pokemon exception handling; do not try to catch them all!
To retrieve the Json Post values you must use request.json
if request.json and 'email' in request.json:
request.json['email']

How do you use cookies and HTTP Basic Authentication in CherryPy?

I have a CherryPy web application that requires authentication. I have working HTTP Basic Authentication with a configuration that looks like this:
app_config = {
'/' : {
'tools.sessions.on': True,
'tools.sessions.name': 'zknsrv',
'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'zknsrv',
'tools.auth_basic.checkpassword': checkpassword,
}
}
HTTP auth works great at this point. For example, this will give me the successful login message that I defined inside AuthTest:
curl http://realuser:realpass#localhost/AuthTest/
Since sessions are on, I can save cookies and examine the one that CherryPy sets:
curl --cookie-jar cookie.jar http://realuser:realpass#localhost/AuthTest/
The cookie.jar file will end up looking like this:
# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This file was generated by libcurl! Edit at your own risk.
localhost FALSE / FALSE 1348640978 zknsrv 821aaad0ba34fd51f77b2452c7ae3c182237deb3
However, I'll get an HTTP 401 Not Authorized failure if I provide this session ID without the username and password, like this:
curl --cookie 'zknsrv=821aaad0ba34fd51f77b2452c7ae3c182237deb3' http://localhost/AuthTest
What am I missing?
Thanks very much for any help.
So, the short answer is you can do this, but you have to write your own CherryPy tool (a before_handler), and you must not enable Basic Authentication in the CherryPy config (that is, you shouldn't do anything like tools.auth.on or tools.auth.basic... etc) - you have to handle HTTP Basic Authentication yourself. The reason for this is that the built-in Basic Authentication stuff is apparently pretty primitive. If you protect something by enabling Basic Authentication like I did above, it will do that authentication check before it checks the session, and your cookies will do nothing.
My solution, in prose
Fortunately, even though CherryPy doesn't have a way to do both built-in, you can still use its built-in session code. You still have to write your own code for handling the Basic Authentication part, but in total this is not so bad, and using the session code is a big win because writing a custom session manager is a good way to introduce security bugs into your webapp.
I ended up being able to take a lot of things from a page on the CherryPy wiki called Simple authentication and access restrictions helpers. That code uses CP sessions, but rather than Basic Auth it uses a special page with a login form that submits ?username=USERNAME&password=PASSWORD. What I did is basically nothing more than changing the provided check_auth function from using the special login page to using the HTTP auth headers.
In general, you need a function you can add as a CherryPy tool - specifically a before_handler. (In the original code, this function was called check_auth(), but I renamed it to protect().) This function first tries to see if the cookies contain a (valid) session ID, and if that fails, it tries to see if there is HTTP auth information in the headers.
You then need a way to require authentication for a given page; I do this with require(), plus some conditions, which are just callables that return True. In my case, those conditions are zkn_admin(), and user_is() functions; if you have more complex needs, you might want to also look at member_of(), any_of(), and all_of() from the original code.
If you do it like that, you already have a way to log in - you just submit a valid session cookie or HTTPBA credentials to any URL you protect with the #require() decorator. All you need now is a way to log out.
(The original code instead has an AuthController class which contains login() and logout(), and you can use the whole AuthController object in your HTTP document tree by just putting auth = AuthController() inside your CherryPy root class, and get to it with a URL of e.g. http://example.com/auth/login and http://example.com/auth/logout. My code doesn't use an authcontroller object, just a few functions.)
Some notes about my code
Caveat: Because I wrote my own parser for HTTP auth headers, it only parses what I told it about, which means just HTTP Basic Auth - not, for example, Digest Auth or anything else. For my application that's fine; for yours, it may not be.
It assumes a few functions defined elsewhere in my code: user_verify() and user_is_admin()
I also use a debugprint() function which only prints output when a DEBUG variable is set, and I've left these calls in for clarity.
You can call it cherrypy.tools.WHATEVER (see the last line); I called it zkauth based on the name of my app. Take care NOT to call it auth, or the name of any other built-in tool, though .
You then have to enable cherrypy.tools.WHATEVER in your CherryPy configuration.
As you can see by all the TODO: messages, this code is still in a state of flux and not 100% tested against edge cases - sorry about that! It will still give you enough of an idea to go on, though, I hope.
My solution, in code
import base64
import re
import cherrypy
SESSION_KEY = '_zkn_username'
def protect(*args, **kwargs):
debugprint("Inside protect()...")
authenticated = False
conditions = cherrypy.request.config.get('auth.require', None)
debugprint("conditions: {}".format(conditions))
if conditions is not None:
# A condition is just a callable that returns true or false
try:
# TODO: I'm not sure if this is actually checking for a valid session?
# or if just any data here would work?
this_session = cherrypy.session[SESSION_KEY]
# check if there is an active session
# sessions are turned on so we just have to know if there is
# something inside of cherrypy.session[SESSION_KEY]:
cherrypy.session.regenerate()
# I can't actually tell if I need to do this myself or what
email = cherrypy.request.login = cherrypy.session[SESSION_KEY]
authenticated = True
debugprint("Authenticated with session: {}, for user: {}".format(
this_session, email))
except KeyError:
# If the session isn't set, it either wasn't present or wasn't valid.
# Now check if the request includes HTTPBA?
# FFR The auth header looks like: "AUTHORIZATION: Basic <base64shit>"
# TODO: cherrypy has got to handle this for me, right?
authheader = cherrypy.request.headers.get('AUTHORIZATION')
debugprint("Authheader: {}".format(authheader))
if authheader:
#b64data = re.sub("Basic ", "", cherrypy.request.headers.get('AUTHORIZATION'))
# TODO: what happens if you get an auth header that doesn't use basic auth?
b64data = re.sub("Basic ", "", authheader)
decodeddata = base64.b64decode(b64data.encode("ASCII"))
# TODO: test how this handles ':' characters in username/passphrase.
email,passphrase = decodeddata.decode().split(":", 1)
if user_verify(email, passphrase):
cherrypy.session.regenerate()
# This line of code is discussed in doc/sessions-and-auth.markdown
cherrypy.session[SESSION_KEY] = cherrypy.request.login = email
authenticated = True
else:
debugprint ("Attempted to log in with HTTBA username {} but failed.".format(
email))
else:
debugprint ("Auth header was not present.")
except:
debugprint ("Client has no valid session and did not provide HTTPBA credentials.")
debugprint ("TODO: ensure that if I have a failure inside the 'except KeyError'"
+ " section above, it doesn't get to this section... I'd want to"
+ " show a different error message if that happened.")
if authenticated:
for condition in conditions:
if not condition():
debugprint ("Authentication succeeded but authorization failed.")
raise cherrypy.HTTPError("403 Forbidden")
else:
raise cherrypy.HTTPError("401 Unauthorized")
cherrypy.tools.zkauth = cherrypy.Tool('before_handler', protect)
def require(*conditions):
"""A decorator that appends conditions to the auth.require config
variable."""
def decorate(f):
if not hasattr(f, '_cp_config'):
f._cp_config = dict()
if 'auth.require' not in f._cp_config:
f._cp_config['auth.require'] = []
f._cp_config['auth.require'].extend(conditions)
return f
return decorate
#### CONDITIONS
#
# Conditions are callables that return True
# if the user fulfills the conditions they define, False otherwise
#
# They can access the current user as cherrypy.request.login
# TODO: test this function with cookies, I want to make sure that cherrypy.request.login is
# set properly so that this function can use it.
def zkn_admin():
return lambda: user_is_admin(cherrypy.request.login)
def user_is(reqd_email):
return lambda: reqd_email == cherrypy.request.login
#### END CONDITIONS
def logout():
email = cherrypy.session.get(SESSION_KEY, None)
cherrypy.session[SESSION_KEY] = cherrypy.request.login = None
return "Logout successful"
Now all you have to do is enable both builtin sessions and your own cherrypy.tools.WHATEVER in your CherryPy configuration. Again, take care not to enable cherrypy.tools.auth. My configuration ended up looking like this:
config_root = {
'/' : {
'tools.zkauth.on': True,
'tools.sessions.on': True,
'tools.sessions.name': 'zknsrv',
}
}

Retrieving contacts with gdata.contacts.client and oauth2

I am using oAuth2WebServerFlow to get an oAuth access token and then retrieve a list of a user's contacts. I'm using web2py as the web framework.
flow = oauth2client.client.OAuth2WebServerFlow(client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
scope='https://www.google.com/m8/feeds',
user_agent=USER_AGENT)
callback = 'http://127.0.0.1:8000/Test/searcher/oauth2callback'
authorise_url = flow.step1_get_authorize_url(callback)
session.flow = pickle.dumps(flow)
redirect(authorise_url)
With the redirect then being handled as follows
flow = pickle.loads(session.flow)
credentials = flow.step2_exchange(request.vars)
My question is how to change the OAuth2Credentials object returned above into an OAuth2AccessToken object, that I can then use to authorise a request to the contacts library with something like:
gc = gdata.contacts.client.ContactsClient(source="")
token.authorize(gc)
gc.GetContacts
I've tried various methods with no success, normally getting an oAuth2AccessTokenError message of "Invalid Grant". I'm thinking something like this may work but also think there must be a simpler way!
token = gdata.gauth.OAuth2Token(client_id=CLIENT_ID, client_secret=CLIENT_SECRET, scope='https://www.google.com/m8/feeds', user_agent=USER_AGENT)
token.redirect_uri = 'http://127.0.0.1:8000/Test/searcher/oauth2callback'
token.get_access_token(<<code to pass the access_token out of the Credentials object??>>)
Can anyone help with this?
I managed to get this working. It was pretty straightforward actually, I just stopped using the OAuth2WebServerFlow, which didn't seem to be adding much value anyway. So the new code looks like this:
token = gdata.gauth.OAuth2Token(client_id, client_secret, scope, ua)
session.token = pickle.dumps(token)
redirect(token.generate_authorize_url(redirect_uri='http://127.0.0.1:8000/Test/default/oauth2callback'))
Followed by
def oauth2callback():
token = pickle.loads(session.token)
token.redirect_uri='http://127.0.0.1:8000/Test/default/oauth2callback'
token.get_access_token(request.vars.code)
gc = gdata.contacts.client.ContactsClient(source='')
gc = token.authorize(gc)
feed = gc.GetContacts()
Hope this is helpful to someoone!
Assuming you have code for newer OAuth2.0 APIs setup correctly, you can get this working by creating a Token class that modifies headers that converts Credentials -> Token class.
OAUTH_LABEL='OAuth '
#Transforms OAuth2 credentials to OAuth2 token.
class OAuthCred2Token(object):
def __init__(self, token_string):
self.token_string = token_string
def modify_request(self, http_request):
http_request.headers['Authorization'] = '%s%s' % (OAUTH_LABEL,
self.token_string)
ModifyRequest = modify_request
You can test it as follows:
gc = gdata.contacts.client.ContactsClient(source='')
token = OAuthCred2Token(creds.access_token)
gc.auth_token = token
print gc.GetContacts()
Note that this code will not handle token refreshes, which code using credentials handles.
In my own application, it is acceptable to make a simple call using a service to refresh the credentials before making a call to get contacts.

Categories