We created an application for Google Apps Marketplace, and we want to add a new feature - get all the domain's user defined schemas. But I didn't find out how to grant our application permission to do it. Do we have to get permission again from all our customers? Or maybe create a new Google Apps Marketplace application and ask permission for it only from the customers who need it? (not all our customers need this feature).
Here is my code, it fails in two ways:
class EmailSettingsClient:
headers = None
domain = None
def __init__(self, admin_email):
self.initCredentials(admin_email)
logging.debug("headers={}".format(self.headers))
def initCredentials(self, admin_email):
http = httplib2.Http()
credentials = SignedJwtAssertionCredentials(
SERVICE_EMAIL,
PRIVATE_KEY,
scope='https://apps-apis.google.com/a/feeds/emailsettings/2.0/ https://www.googleapis.com/auth/admin.directory.user.readonly https://www.googleapis.com/auth/admin.directory.userschema.readonly',
sub=str(admin_email) # it doesn't work with this line.
)
http = credentials.authorize(http)
service = build(serviceName='admin', version='directory_v1', http=http)
creds = credentials.to_json()
access_token = credentials.token_response['access_token']
self.headers = {
'Content-type': 'application/atom+xml',
'Authorization': 'Bearer '+access_token
}
def get_all_domain_custom_schemas(self, feed_uri):
customer_id = "..." # domain customerId
logging.debug("EmailSettingsClient::get_all_domain_custom_schemas, feed_uri = {}, customerId = {} .".format(feed_uri, customer_id))
request_url = 'https://www.googleapis.com/admin/directory/v1/customer/{}/schemas'.format(customer_id)
reply = requests.get(request_url, headers=self.headers)
# data = json.loads(reply.content)
logging.info(reply.content)
customer_id = "my_customer"
logging.debug("EmailSettingsClient::get_all_domain_custom_schemas, feed_uri = {}, customerId = {} .".format(feed_uri, customer_id))
request_url = 'https://www.googleapis.com/admin/directory/v1/customer/{}/schemas'.format(customer_id)
reply = requests.get(request_url, headers=self.headers)
# data = json.loads(reply.content)
logging.info(reply.content)
class GetAllDomainCustomSchemasHandler(RequestHandler):
def post(self):
return self.get()
def get(self):
domain_str = self.request.get('domain')
domain = Domain.get_by_key_name(domain_str)
feed_uri = self.request.get('feed_uri', "")
email_settings_client = EmailSettingsClient(domain.admin)
email_settings_client.get_all_domain_custom_schemas(feed_uri)
return
One, it throws an exception if we use the "sub" parameter from another user with the domain admin's email address. And Two, if I don't include the "sub" parameter and run the application from the domain admin's account, it returns an error message: When I tried with the domain customerId, I got this error message:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "forbidden",
"message": "Not Authorized to access this resource/api"
}
],
"code": 403,
"message": "Not Authorized to access this resource/api"
}
}
And when I tried with "my_customer", I got this error message:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "backendError",
"message": "Service unavailable. Please try again"
}
],
"code": 503,
"message": "Service unavailable. Please try again"
}
}
I'm sure I missed something, but I didn't find out how to make it work. Any suggestions?
Eventually I found out that we can add this permission in the Google Developers Console, under APIs / Google Apps Marketplace SDK / Configuration. I added https://www.googleapis.com/auth/admin.directory.userschema.readonly, and each domain admin has to approve the new permissions. If you have more information you can edit this answer or post a new answer.
Related
I am trying to send an e-mail using the Gmail API with my service account. I have several scopes, and am able to create a draft e-mail, but when I try to send an e-mail I get a 401 response back stating I am missing required authentication credentials? I've searched and searched but I'm just not sure what I need to do here to get scope recognized, or to pass the correct scope/credentials. Here are my scopes (I went crazy adding):
Scope screenshot from Google
Here is my code (essentially straight from the google tutorial):
def gmail_send_message():
"""Create and send an email message
Print the returned message id
Returns: Message object, including message id
Load pre-authorized user credentials from the environment.
TODO(developer) - See https://developers.google.com/identity
for guides on implementing OAuth2 for the application.
"""
SERVICE_ACCOUNT_FILE = 'python_email_key.json'
creds = service_account.Credentials.from_service_account_file(
filename=SERVICE_ACCOUNT_FILE,
scopes=['https://mail.google.com/',
'https://www.googleapis.com/auth/gmail.compose',
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.send'],
subject='Test#mydomain.com' # <--Changed for privacy in this post
)
try:
# create gmail api client
service = build('gmail', 'v1', credentials=creds)
message = MIMEText('This is automated mail. 12345')
message['To'] = 'Test#mydomain.com' # <--Changed for privacy in this post
message['From'] = 'Test#mydomain.com' # <--Changed for privacy in this post
message['Subject'] = 'Automated draft'
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
create_message = {
'message': {
'raw': encoded_message
}
}
# pylint: disable=E1101
draft = service.users().drafts().create(userId="me", body=create_message).execute() #This works perfectly
print(F'Draft id: {draft["id"]}\nDraft message: {draft["message"]}')
send_message = (service.users().messages().send(userId="me", body=create_message).execute()) #This fails
print(F'Message Id: {send_message["id"]}')
except HttpError as error:
print(F'An error occurred: {error}')
send_message = None
return send_message
Here is the response:
{
"error": {
"code": 401,
"message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"errors": [
{
"message": "Login Required.",
"domain": "global",
"reason": "required",
"location": "Authorization",
"locationType": "header"
}
],
"status": "UNAUTHENTICATED",
"details": [
{
"#type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "CREDENTIALS_MISSING",
"domain": "googleapis.com",
"metadata": {
"service": "gmail.googleapis.com",
"method": "caribou.api.proto.MailboxService.GetMessage"
}
}
]
}
}
I'm following the Get access without a user guide to write a Python script that will call Microsoft Graph.
This script will be scheduled from cron so it cannot get admin consent (therefore authorize using Client Credentials). I am able to successfully obtain a token using this call:
request_url = "https://login.microsoftonline.com/mytenant.onmicrosoft.com/oauth2/v2.0/token"
data = {
'Host' : 'login.microsoftonline.com',
'Content-Type' : 'application/x-www-form-urlencoded',
'client_id' : 'my-client-id-1234',
'scope' : 'https://graph.microsoft.com/.default',
'client_secret' : client_secret,
'grant_type' : 'client_credentials'
}
response = requests.post(url = request_url, data = data)
I then try to get a user listing with this call, using the valid token:
request_url = "https://graph.microsoft.com/v1.0/users"
headers = {
'Authorization' : 'Bearer ' + token,
'Host' : 'graph.microsoft.com'
}
response = requests.get(url = request_url, headers = headers)
The problem is that I get an Authorization_IdentityNotFound error:
<Response [401]>
{
"error": {
"code": "Authorization_IdentityNotFound",
"message": "The identity of the calling application could not be established.",
"innerError": {
"request-id": "2257f532-abc4-4465-b19f-f33541787e76",
"date": "2018-03-27T19:11:07"
}
}
}
These are the permissions I've selected:
Any idea how to fix this error?
For others running into this issue, I was also getting this error until found out the documentation omits a very important caveat:
For client credentials, if the app belongs to a work or school (organization) context then for https://login.microsoftonline.com/common/oauth2/token replace common with a tenantId or domain name
See
Authorization_IdentityNotFound on Microsoft Graph API request
First things first, you can go ahead an remove all those Delegated Permission scopes. If you're using the Client Credentials Grant, you will only be using Application Permission scopes.
Second, you need to execute the Admin Consent flow before you can use Client Credentials. This is done by having a Global Admin from the tenant authenticate and accept your scope request:
https://login.microsoftonline.com/common/adminconsent?client_id=[APPLICATION ID]&redirect_uri=[REDIRECT URI]
You can read more about Admin Consent here: v2 Endpoint and Admin Consent
So I am trying to obtain some data from Firebase, which ofcourse has some rules/security constraints defined. I can authenticate my user from the Login & Auth tab, and I'd like to get data from firebase, however my user is still not authenticated.
user = ref.authenticate(email, password) , which returns the following for user
{
u'token':{some long token here}',
u'user':{
u'uid':u'ef44b781-8842-4f28-abf0-2ac9aa0b2bea',
u'provider':u'password',
u'email':u'user#email.com',
u'isTemporaryPassword':False,
u'sessionKey':u'{session key here}}',
u'md5_hash':u'{md5_hash here}}',
u'id':u'ef44b781-8842-4f28-abf0-2ac9aa0b2bea'
}
}
Now that I know the user is authenticated (otherwise it returns something along the lines of an error, I would like to do a simple GET conversations = firebase.get(FIREBASE_NAME + '/conversations/' + me), where 'me' is the user['user']['uid']
I have the following structure for conversations:
conversations/my-uid/other-uid/{data}
I would think my user is authenticated, still it returns a PermissionDenied
EDIT
Solved this by using a different library you can find here. The initial library I used did not support authentication, while this one does. The way it was solved, was by implementing some functions from the other one and sending my token as follow:
FIREBASE.child('/messages/').get(token=token)
You should not send passwords in the URL, you can do this way:
__FIREBASE_USER_VERIFY_SERVICE = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword"
__FIREBASE_API_KEY = __your_api_key_here__
def user_login(email, passwd):
url = "%s?key=%s" % (__FIREBASE_USER_VERIFY_SERVICE, __FIREBASE_API_KEY)
data = {"email": email,
"password": passwd,
"returnSecureToken": True}
result = requests.post(url, json=data)
is_login_successful = result.ok
json_result = result.json()
return json_result # authToken=> json_result['idToken']
If successful, it will result like:
{
"displayName": "",
"email": "your_users_email#example.com",
"expiresIn": "3600",
"idToken": "abc123...",
"kind": "identitytoolkit#VerifyPasswordResponse",
"localId": "UWQ...x2",
"refreshToken": "def098...",
"registered": true
}
If fails (wrong password etc) :
{
"error": {
"code": 400,
"errors": [
{
"domain": "global",
"message": "INVALID_PASSWORD",
"reason": "invalid"
}
],
"message": "INVALID_PASSWORD"
}
}
or may be
{
"error": {
"code": 400,
"errors": [
{
"domain": "global",
"message": "MISSING_PASSWORD",
"reason": "invalid"
}
],
"message": "MISSING_PASSWORD"
}
}
Solved this by using a different library you can find here. The initial library I used did not support authentication, while this one does. The way it was solved, was by implementing some functions from the other one and authenticate as follows:
def auth_with_password(self, email, password):
request_ref = 'https://auth.firebase.com/auth/firebase?firebase={0}&email={1}&password={2}'.\
format(self.fire_base_name, email, password)
request_object = self.requests.get(request_ref)
return request_object.json()
Then to make an authorized call, do this
user = auth_with_password(email, password)
token = user['user']['token']
FIREBASE.child('/messages/').get(token=token)
Make sure your token is correct. The library supports this, but otherwise I would suggest that you use Firebase token generator for Python
When you create an instance A in Google Compute Engine, it'll get predefined, "default" service account attached to it (this basically means, that you can query google API from A, being authenticated with 'default' service account).
What I'd like to do, is to setup GCE instance with service account, that's different than a default one. This should be conceptually possible, given GCE API, but fails with exception:
{
"name": "operation-1400060483459-4f958fbc7d7b9-cd817778-b80d1cad",
"operationType": "insert",
"status": "DONE",
"user": "some_name#developer.gserviceaccount.com",
"error": {
"errors": [ {
"code": "SERVICE_ACCOUNT_ACCESS_DENIED",
"message": "The user does not have access to service account 'some_name#developer.gserviceaccount.com'"
} ] } }
Here's my code in python, which setups the instance:
discovery_service = discovery.build('compute',
config['compute_api_version'],
http=SignedJwtAssertionCredentials(
service_account_name="some_name#developer.gserviceaccount.com",
private_key=key_data,
scope='https://www.googleapis.com/auth/compute')
.authorize(httplib2.Http()))
instance = {}
# sets instance configuration details here
# ...
# ...
instance['serviceAccounts'] = [{
'email': "some_name#developer.gserviceaccount.com",
'scopes': ['https://www.googleapis.com/auth/devstorage.full_control',
'https://www.googleapis.com/auth/compute',
'https://www.googleapis.com/auth/userinfo.email', ]
}]
discovery_service.instances().insert(project=project, zone=zone, body=instance)
The weirdest part of it, is that exception says "The user does not have access to service account 'some_name#developer.gserviceaccount.com'", but the "user" it refers to is the 'some_name#developer.gserviceaccount.com' itself! Which means 'some_name#developer.gserviceaccount.com' does not have access to 'some_name#developer.gserviceaccount.com', which makes no sense.
I believe you'll need to create a new service account to use the API from a non-GCE instance. The service account you're referencing works within a GCE instance only.
To do that go to the Cloud Console > Project > APIs & Auth > Credentials.
Create new Client ID
Service Account
Download the .p12 file and load that as the private key. (See example below)
Also you'll need to create an instance from a boot disk which is typically created from one of the GCE supplied images.
Here's an example using JSON Web Tokens that worked for me. It was adapted from the docs located here: https://cloud.google.com/compute/docs/tutorials/python-guide#addinganinstance.
from apiclient import discovery
from oauth2client.file import Storage
from oauth2client.client import SignedJwtAssertionCredentials
import httplib2
import os.path
INSTANCE_NAME = 'my-instance'
API_VERSION = 'v1'
GCE_URL = 'https://www.googleapis.com/compute/%s/projects/' % (API_VERSION)
PROJECT_ID = '***'
SERVICE_ACOUNT_CLIENT_ID = '***.apps.googleusercontent.com'
SERVICE_ACCOUNT_EMAIL_ADDRESS = '***#developer.gserviceaccount.com'
GCE_SCOPE = 'https://www.googleapis.com/auth/compute'
ZONE = 'us-central1-a'
DEFAULT_SERVICE_EMAIL = 'default'
DEFAULT_SCOPES = ['https://www.googleapis.com/auth/devstorage.full_control',
'https://www.googleapis.com/auth/compute']
SOURCE_IMAGE_URL = 'projects/ubuntu-os-cloud/global/images/ubuntu-1410-utopic-v20141217'
def main():
f = file('private-key.p12', 'rb')
oauth_key_data = f.read()
f.close()
http = httplib2.Http()
oauth_storage = Storage('compute-creds.dat')
oauth_credentials = oauth_storage.get()
if oauth_credentials is None or oauth_credentials.invalid:
oauth_credentials = SignedJwtAssertionCredentials(
service_account_name=SERVICE_ACCOUNT_EMAIL_ADDRESS,
private_key=oauth_key_data,
scope=GCE_SCOPE)
oauth_storage.put(oauth_credentials)
else:
oauth_credentials.refresh(http)
http = oauth_credentials.authorize(http)
gce_service = discovery.build('compute', 'v1', http=http)
project_url = '%s%s' % (GCE_URL, PROJECT_ID)
image_url = '%s%s/global/images/%s' % (
GCE_URL, 'ubuntu-os-cloud', 'ubuntu-1410-utopic-v20141217')
machine_type_url = '%s/zones/%s/machineTypes/%s' % (
project_url, ZONE, 'n1-standard-1')
network_url = '%s/global/networks/%s' % (project_url, 'default')
instance = {
'name': INSTANCE_NAME,
'machineType': machine_type_url,
'disks': [{
'autoDelete': 'true',
'boot': 'true',
'type': 'PERSISTENT',
'initializeParams' : {
'diskName': INSTANCE_NAME,
'sourceImage': SOURCE_IMAGE_URL
}
}],
'networkInterfaces': [{
'accessConfigs': [{
'type': 'ONE_TO_ONE_NAT',
'name': 'External NAT'
}],
'network': network_url
}],
'serviceAccounts': [{
'email': DEFAULT_SERVICE_EMAIL,
'scopes': DEFAULT_SCOPES
}]
}
# Create the instance
request = gce_service.instances().insert(
project=PROJECT_ID, body=instance, zone=ZONE)
response = request.execute(http=http)
response = _blocking_call(gce_service, http, response)
print response
def _blocking_call(gce_service, auth_http, response):
"""Blocks until the operation status is done for the given operation."""
status = response['status']
while status != 'DONE' and response:
operation_id = response['name']
# Identify if this is a per-zone resource
if 'zone' in response:
zone_name = response['zone'].split('/')[-1]
request = gce_service.zoneOperations().get(
project=PROJECT_ID,
operation=operation_id,
zone=zone_name)
else:
request = gce_service.globalOperations().get(
project=PROJECT_ID, operation=operation_id)
response = request.execute(http=auth_http)
if response:
status = response['status']
return response
main()
FYI: in GCE you usually get two default service accounts:
-compute#developer.gserviceaccount.com
#cloudservices.gserviceaccount.com
Note the different Email suffix (developer.gserviceaccount.com vs. cloudservices.gserviceaccount.com). It appears that using your own service account, EVEN if it has the Owner role, does not grant you access to the <number>#cloudservices.gserviceaccount.com account, only to the 1st one (<number>-compute#developer.gserviceaccount.com).
In my case, I got the aforementioned error when trying to create an instance with my own service account while specifing that the instance will use the 2nd service account from above. Once I fixed the call to request that the instance will use the 1st account, it worked.
Sending a DELETE request to Blogger REST API (v3.0), I'm trying to delete a post using delete method. For this I use the following code:
api_uri = 'https://www.googleapis.com/blogger/v3/blogs/%s/posts/%s' % (blogId, postId)
result = urlfetch.fetch(url=api_uri,
method=urlfetch.DELETE,
headers={'Authorization' : oauth_token})
self.response.out.write(result.content)
But the server returns:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "notFound",
"message": "Not Found"
}
],
"code": 404,
"message": "Not Found"
}
}
However, I can retrieve information about this post, using the following code:
api_uri = 'https://www.googleapis.com/blogger/v3/blogs/%s/posts/%s' % (blogId, postId)
result = urlfetch.fetch(url=api_uri,
headers={'Authorization' : oauth_token})
self.response.out.write(result.content)
At this moment, I can't understand what am I doing wrong — the request is authorized, the blogId and postId are correct — but anyway, the server returns "not found" error.
If you know how to solve this problem or you can give useful advice — help me please. Thank you for your time and consideration of this matter.
UPD 1: If I send requests to the following URLs:
# https://www.googleapis.com/blogger/v3/users/{userID}
# https://www.googleapis.com/blogger/v3/users/self
The server also returns:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "notFound",
"message": "Not Found"
}
],
"code": 404,
"message": "Not Found"
}
}
UPD 2: I forgot to say that I'm using OAuth 2.0 for Server to Server Applications. Thus, to get authorization token, I send request to https://accounts.google.com/o/oauth2/token using the following JWT Claim Set:
jwt_claim_set = {
'iss' : '{id}#developer.gserviceaccount.com',
'scope' : 'https://www.googleapis.com/auth/blogger',
'aud' : 'https://accounts.google.com/o/oauth2/token',
'exp' : expire,
'iat' : timestamp
}
The server returns:
{
"access_token" : "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
"token_type" : "Bearer",
"expires_in" : 3600
}
And define variable oauth_token, using:
data = simplejson.loads(result.content)
oauth_token = data['token_type'] + ' ' + data['access_token']
Are you sure that you're using OAuth2 properly? It seems to me that you're not properly logged in, and that's why you're getting those errors.
Try those same queries using Google OAuh2 Playground (https://code.google.com/oauthplayground/) and see what happens.