SecretManagerServiceClient in Google Cloud Run and authentication via service account - python-3.x

I can create a SecretManagerServiceClient without using a key file successfully in Google Cloud Shell:
from google.cloud import secretmanager
from google.oauth2 import service_account
from google.auth.exceptions import DefaultCredentialsError
import logging
import sys
import os
def list_secrets(client, project_id):
"""
Retrieve all secrets associated with a project
:param project_id: the alpha-numeric name of the project
:return: a generator of Secrets
"""
try:
secret_list = client.list_secrets(request={"parent": "projects/{}".format(project_id)})
except Exception as e:
sys.exit("Did not successfully retrieve secret list.")
return secret_list
def set_env_secrets(client, secret_ids, label=None):
"""
Sets secrets retrieved from Google Secret Manager in the runtime environment
of the Python process
:param secret_ids: a generator of Secrets
:param label: Secrets with this label will be set in the environment
"""
for s in secret_ids:
# we only want secrets with matching labels (or all of them if label wasn't specified)
if not label or label in s.labels:
version = client.access_secret_version(request={'name': '{}/versions/latest'.format(s.name)})
payload_str = version.payload.data.decode("UTF-8")
os.environ[s.name.split('/')[-1]] = payload_str
if __name__ == "__main__":
client = secretmanager.SecretManagerServiceClient()
secrets = list_secrets(client, "myprojectid-123456")
set_env_secrets(client, secrets)
print(os.getenv("DATA_DB_HOST"))
However, when I use similar code as the basis for an entry point of a container in Google Cloud Run, the attempt to retrieve a client using the default service account's credentials fails with
File "entry_point.py", line 27, in get_client
client = secretmanager.SecretManagerServiceClient()
File "/usr/local/lib/python3.6/site-packages/google/cloud/secretmanager_v1/services/secret_manager_service/client.py", line 274, in __init__
client_info=client_info,
File "/usr/local/lib/python3.6/site-packages/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py", line 162, in __init__
scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id
File "/usr/local/lib/python3.6/site-packages/google/auth/_default.py", line 340, in default
credentials, project_id = checker()
File "/usr/local/lib/python3.6/site-packages/google/auth/_default.py", line 186, in _get_explicit_environ_credentials
os.environ[environment_vars.CREDENTIALS]
File "/usr/local/lib/python3.6/site-packages/google/auth/_default.py", line 97, in load_credentials_from_file
"File {} was not found.".format(filename)
google.auth.exceptions.DefaultCredentialsError: File was not found.
The default service account has the Editor and Secret Manager Admin roles (thanks to #DanielOcando for his comment). Why is it that the ADC library, as described here does not pick up the permissions of the default service account and use them to instantiate the client?
Update 1
#guillaumeblaquiere asked about dependencies. The container is built with Python 3.6.12 and the following libraries:
Django==2.1.15
django-admin-rangefilter==0.3.7
django-extensions==2.1.2
django-ipware==1.1.6
pytz==2017.3
psycopg2==2.7.3.2
waitress==1.4.1
geoip2==2.6
gunicorn==19.9.0
social-auth-app-django==3.1.0
semver==2.8.1
sentry-sdk==0.6.9
google-api-core==1.23.0
google-auth==1.23.0
google-cloud-secret-manager==2.0.0
I created a custom service account, added Editor and Secret Manager Admin roles to it, and then used the Console to deploy a new revision with that account, but the same error resulted.
Update 2
Thinking that matching the CPython version in Cloud Shell would do the trick, I rebuilt the image with Python 3.7. No luck.
Update 3
Taking a different tack, I added Service Account Token Creator role to the default service account of the project and created a terraform file and configured it for service account impersonation. I also ran gcloud auth application-default login in the shell prior to invoking terraform.
provider "google" {
alias = "tokengen"
}
data "google_client_config" "default" {
provider = google.tokengen
}
data "google_service_account_access_token" "sa" {
provider = "google.tokengen"
target_service_account = "XXXXXXXXXXXX-compute#developer.gserviceaccount.com"
lifetime = "600s"
scopes = [
"https://www.googleapis.com/auth/cloud-platform",
]
}
provider "google" {
project = "myprojectid-123456"
region = "us-central1"
zone = "us-central1-f"
#impersonate_service_account = "XXXXXXXXXXXX-compute#developer.gserviceaccount.com
}
resource "google_cloud_run_service" "default" {
name = "myprojectid-123456"
location = "us-central1"
template {
spec {
containers {
image = "us.gcr.io/myprojectid-123456/testimage"
}
}
}
traffic {
percent = 100
latest_revision = true
}
}
This did work to create the service, but again, when the endpoint attempted to instantiate SecretManagerServiceClient, the same error resulted.

Related

Authorization for Airflow 2.0 and Azure AD using custom role - Override oauth_user_info method in AirflowSecurityManager

I am deploying Airflow 2.0 on Azure and using Azure AD for Authentication and Authorization.
Created App registration and Custom roles. Authentication with AD works. need to read role claims in token to do the role mapping .. Below is my web Config ... For this I am overriding oauth_user_info method in AirflowSecurityManager to read the roles claim in Azure AD token and assign it to role_keys..
Problem is when I do this and assign 'SECURITY_MANAGER_CLASS = AzureCustomSecurity, although web UI POD gets up without any error but when I test it appears that custom class method oauth_user_info is not called at all ... if I remove the custom class then I can see authentication works and I can see the token in logs.... so problem is that this custom class in not invoked in the authentication & Authorization process. Any one can help here ?
I can see a similar kind of thing working for AWS congnito in a post here
AWS Cognito OAuth configuration for Flask Appbuilder
This is my Web config
import os
import json
from airflow.configuration import conf
from flask_appbuilder.security.manager import AUTH_OAUTH
from airflow.www.security import AirflowSecurityManager
from customsecmanager import AzureDlabSecurity
SQLALCHEMY_DATABASE_URI = conf.get("core", "SQL_ALCHEMY_CONN")
basedir = os.path.abspath(os.path.dirname(__file__))
CSRF_ENABLED = True
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH_USER_REGISTRATION = True
class AzureCustomSecurity(AirflowSecurityManager):
def oauth_user_info(self, provider, response=None):
log.debug("inside custom method")
if provider == "azure":
log.debug("Azure response received : {0}".format(resp))
id_token = resp["id_token"]
log.debug(str(id_token))
me = self._azure_jwt_token_parse(id_token)
log.debug("Parse JWT token : {0}".format(me))
return {
"name": me["name"],
"email": me["upn"],
"first_name": me["given_name"],
"last_name": me["family_name"],
"id": me["oid"],
"username": me["oid"],
"role_keys": me["roles"],
}
else:
return {}
OAUTH_PROVIDERS = [{
'name':'azure',
'token_key':'access_token',
'icon':'fa-windows',
'remote_app': {
'base_url':'https://graph.microsoft.com/v1.0/',
'request_token_params' :{'scope': 'openid'},
'access_token_url':'https://login.microsoftonline.com/<tenantid>/oauth2/token',
'authorize_url':'https://login.microsoftonline.com/<tenantid>/oauth2/authorize',
'request_token_url': None,
'client_id':'XXXXXXXX',
'client_secret':'******'
}
}]
# a mapping from the values of `userinfo["role_keys"]` to a list of FAB roles
AUTH_ROLES_MAPPING = {
"Viewer": ["Viewer"],
"Admin": ["Admin"],
#}
AUTH_ROLES_SYNC_AT_LOGIN = True
SECURITY_MANAGER_CLASS = AzureCustomSecurity
I tried to reproduce your code, but instead of overriding oauth_user_info I've overridden get_oauth_user_info method (docs link) and it worked. May you try this?
class AzureCustomSecurity(AirflowSecurityManager):
def get_oauth_user_info(self, provider, resp):
""" you code here... """

How to refresh the boto3 credetials when python script is running indefinitely

I am trying to write a python script that uses watchdog to look for file creation and upload that to s3 using boto3. However, my boto3 credentials expire after every 12hrs, So I need to renew them. I am storing my boto3 credentials in ~/.aws/credentials. So right now I am trying to catch the S3UploadFailedError, renew the credentials, and write them to ~/.aws/credentials. But though the credentials are getting renewed and I am calling boto3.client('s3') again its throwing exception.
What am I doing wrong? Or how can I resolve it?
Below is the code snippet
try:
s3 = boto3.client('s3')
s3.upload_file(event.src_path,'bucket-name',event.src_path)
except boto3.exceptions.S3UploadFailedError as e:
print(e)
get_aws_credentials()
s3 = boto3.client('s3')
I have found a good example to refresh the credentials within this link:
https://pritul95.github.io/blogs/boto3/2020/08/01/refreshable-boto3-session/
but there this a little bug inside. Be careful about that.
Here is the corrected code:
from uuid import uuid4
from datetime import datetime
from time import time
from boto3 import Session
from botocore.credentials import RefreshableCredentials
from botocore.session import get_session
class RefreshableBotoSession:
"""
Boto Helper class which lets us create refreshable session, so that we can cache the client or resource.
Usage
-----
session = RefreshableBotoSession().refreshable_session()
client = session.client("s3") # we now can cache this client object without worrying about expiring credentials
"""
def __init__(
self,
region_name: str = None,
profile_name: str = None,
sts_arn: str = None,
session_name: str = None,
session_ttl: int = 3000
):
"""
Initialize `RefreshableBotoSession`
Parameters
----------
region_name : str (optional)
Default region when creating new connection.
profile_name : str (optional)
The name of a profile to use.
sts_arn : str (optional)
The role arn to sts before creating session.
session_name : str (optional)
An identifier for the assumed role session. (required when `sts_arn` is given)
session_ttl : int (optional)
An integer number to set the TTL for each session. Beyond this session, it will renew the token.
50 minutes by default which is before the default role expiration of 1 hour
"""
self.region_name = region_name
self.profile_name = profile_name
self.sts_arn = sts_arn
self.session_name = session_name or uuid4().hex
self.session_ttl = session_ttl
def __get_session_credentials(self):
"""
Get session credentials
"""
session = Session(region_name=self.region_name, profile_name=self.profile_name)
# if sts_arn is given, get credential by assuming given role
if self.sts_arn:
sts_client = session.client(service_name="sts", region_name=self.region_name)
response = sts_client.assume_role(
RoleArn=self.sts_arn,
RoleSessionName=self.session_name,
DurationSeconds=self.session_ttl,
).get("Credentials")
credentials = {
"access_key": response.get("AccessKeyId"),
"secret_key": response.get("SecretAccessKey"),
"token": response.get("SessionToken"),
"expiry_time": response.get("Expiration").isoformat(),
}
else:
session_credentials = session.get_credentials().__dict__
credentials = {
"access_key": session_credentials.get("access_key"),
"secret_key": session_credentials.get("secret_key"),
"token": session_credentials.get("token"),
"expiry_time": datetime.fromtimestamp(time() + self.session_ttl).isoformat(),
}
return credentials
def refreshable_session(self) -> Session:
"""
Get refreshable boto3 session.
"""
# get refreshable credentials
refreshable_credentials = RefreshableCredentials.create_from_metadata(
metadata=self.__get_session_credentials(),
refresh_using=self.__get_session_credentials,
method="sts-assume-role",
)
# attach refreshable credentials current session
session = get_session()
session._credentials = refreshable_credentials
session.set_config_variable("region", self.region_name)
autorefresh_session = Session(botocore_session=session)
return autorefresh_session
According to the documentation, the client looks in several locations for credentials and there are other options that are also more programmatic-friendly that you might want to consider instead of the .aws/credentials file.
Quoting the docs:
The order in which Boto3 searches for credentials is:
Passing credentials as parameters in the boto.client() method
Passing credentials as parameters when creating a Session object
Environment variables
Shared credential file (~/.aws/credentials)
AWS config file (~/.aws/config)
Assume Role provider
In your case, since you are already catching the exception and renewing the credentials, I would simply pass the new ones to a new instance of the client like so:
client = boto3.client(
's3',
aws_access_key_id=NEW_ACCESS_KEY,
aws_secret_access_key=NEW_SECRET_KEY,
aws_session_token=NEW_SESSION_TOKEN
)
If instead you are using these same credentials elsewhere in the code to create other clients, I'd consider setting them as environment variables:
import os
os.environ['AWS_ACCESS_KEY_ID'] = NEW_ACCESS_KEY
os.environ['AWS_SECRET_ACCESS_KEY'] = NEW_SECRET_KEY
os.environ['AWS_SESSION_TOKEN'] = NEW_SESSION_TOKEN
Again, quoting the docs:
The session key for your AWS account [...] is only needed when you are using temporary credentials.
Here is my implementation which only generates new credentials if existing credentials expire using a singleton design pattern
import boto3
from datetime import datetime
from dateutil.tz import tzutc
import os
import binascii
class AssumeRoleProd:
__credentials = None
def __init__(self):
assert True==False
#staticmethod
def __setCredentials():
print("\n\n ======= GENERATING NEW SESSION TOKEN ======= \n\n")
# create an STS client object that represents a live connection to the
# STS service
sts_client = boto3.client('sts')
# Call the assume_role method of the STSConnection object and pass the role
# ARN and a role session name.
assumed_role_object = sts_client.assume_role(
RoleArn=your_role_here,
RoleSessionName=f"AssumeRoleSession{binascii.b2a_hex(os.urandom(15)).decode('UTF-8')}"
)
# From the response that contains the assumed role, get the temporary
# credentials that can be used to make subsequent API calls
AssumeRoleProd.__credentials = assumed_role_object['Credentials']
#staticmethod
def getTempCredentials():
credsExpired = False
# Return object for the first time
if AssumeRoleProd.__credentials is None:
AssumeRoleProd.__setCredentials()
credsExpired = True
# Generate if only 5 minutes are left for expiry. You may setup for entire 60 minutes by catching botocore ClientException
elif (AssumeRoleProd.__credentials['Expiration']-datetime.now(tzutc())).seconds//60<=5:
AssumeRoleProd.__setCredentials()
credsExpired = True
return AssumeRoleProd.__credentials
And then I am using singleton design pattern for client as well which would generate a new client only if new session is generated. You can add region as well if required.
class lambdaClient:
__prodClient = None
def __init__(self):
assert True==False
#staticmethod
def __initProdClient():
credsExpired, credentials = AssumeRoleProd.getTempCredentials()
if lambdaClient.__prodClient is None or credsExpired:
lambdaClient.__prodClient = boto3.client('lambda',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken'])
return lambdaClient.__prodClient
#staticmethod
def getProdClient():
return lambdaClient.__initProdClient()

Python Azure Function - System Assigned Identity - Login timeout expired (0) (SQLDriverConnect)

I have a requirement to use Azure Managed Identity to connect to Azure SQL from my Azure Function which is written in Python. But when I am deploying the code below, I am getting the following exception:
Exception: OperationalError: ('HYT00', '[HYT00] [Microsoft][ODBC Driver 17 for SQL Server]Login timeout expired (0) (SQLDriverConnect)')
Stack: File "/azure-functions-host/workers/python/3.7/LINUX/X64/azure_functions_worker/dispatcher.py", line 338, in _handle__invocation_request
self.__run_sync_func, invocation_id, fi.func, args)
File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 57, in run
result = self.fn(*self.args, **self.kwargs)
File "/azure-functions-host/workers/python/3.7/LINUX/X64/azure_functions_worker/dispatcher.py", line 470, in __run_sync_func
return func(**params)
File "/home/site/wwwroot/ManagedIdenityTester/__init__.py", line 33, in main
connection = pyodbc.connect(odsConnectStr)
Here is a code snippet:
import pyodbc, logging
def main(mytimer: func.TimerRequest) -> None:
logging.info(
'+++++++++++++++++++++ Python HTTP trigger function processed a request.'
)
odsConnectStr = "Driver={ODBC Driver 17 for SQL Server};Server=tcp:my-server.database.windows.net,1433;Database=my_db;Connection Timeout=30;Authentication=ActiveDirectoryMsi"
logging.info(f'--------- {odsConnectStr}')
connection = pyodbc.connect(odsConnectStr)
cursor = connection.cursor()
cursor.execute("SELECT ##version;")
row = cursor.fetchone()
while row:
logging.info(f'--------- {row[0]}')
row = cursor.fetchone()
So can anyone help on how I can connect to Azure SQL by my Azure Function using the managed identity?
PS: I have followed all the steps to enable system identity and also have created the external user within Azure SQL.
If you want to use Azure MSI to connect Azure SQL in Azure function with python, you can use Azure MSI to get Azure AD access token then you can use the token to connect AzureSQL. But please note that before you use the MSI to connect Azure SQL, you need to add the MSI as Azure SQL Database contained user and configure the needed SQL permissions for the MSI.
For example
Enable system-assigned identity for your Azure Function
Add the MSi as contained users in your Azure SQL database
a. Connect your SQL database with Azure SQL AD admin (I use SSMS to do it)
b. run the following the script in your database
CREATE USER <your app service name> FROM EXTERNAL PROVIDER;
// configure read and write permissions for the MSI
ALTER ROLE db_datareader ADD MEMBER <your app service name>
ALTER ROLE db_datawriter ADD MEMBER <your app service name>
Code
import os
import pyodbc
import requests
import struct
#get access token
identity_endpoint = os.environ["IDENTITY_ENDPOINT"]
identity_header = os.environ["IDENTITY_HEADER"]
resource_uri="https://database.windows.net/"
token_auth_uri = f"{identity_endpoint}?resource={resource_uri}&api-version=2019-08-01"
head_msi = {'X-IDENTITY-HEADER':identity_header}
resp = requests.get(token_auth_uri, headers=head_msi)
access_token = resp.json()['access_token']
accessToken = bytes(access_token, 'utf-8');
exptoken = b"";
for i in accessToken:
exptoken += bytes({i});
exptoken += bytes(1);
tokenstruct = struct.pack("=i", len(exptoken)) + exptoken;
conn = pyodbc.connect("Driver={ODBC Driver 17 for SQL Server};Server=tcp:<>.database.windows.net,1433;Database=<>", attrs_before = { 1256:bytearray(tokenstruct) });
cursor = conn.cursor()
cursor.execute("select ##version")
row = cursor.fetchone()
while row:
logging.info(f'--------- {row[0]}')
row = cursor.fetchone()
For more details, please refer to here and here

Dataproc python API error permission denied

I try to create a dataproc cluster via python API, I use authentification with json fle containing credentials.
app = Flask(__name__)
# Explicitly use service account credentials by specifying the private key
# file.
credentials_gcp =
service_account.Credentials.from_service_account_file('credentials.json')
client = dataproc_v1.ClusterControllerClient(credentials = credentials_gcp)
clustertest = {
"project_id": "xxxx",
"cluster_name": "testcluster",
"config": {}
}
# launch cluster on Dataproc
#app.route('/cluster/<project_id>/<region>/<clustername>', methods=['POST'])
def cluster(project_id, region, clustername):
response = client.create_cluster(project_id, 'regions/europe-west1-b',
clustertest)
response.add_done_callback(callback)
result = response.metadata()
return jsonify(result)
I get the following error
google.api_core.exceptions.PermissionDenied: 403 Permission denied on 'locations/regions/europe-west1' (or it may not exist)
I don't know if I don't have the correct rights or I have an error in the syntax
I managed to solve the issue with adding the zone when instantiating the client:
your_region = "europe-west1"
client_cluster = dataproc_v1.ClusterControllerClient(credentials = credentials_gcp, client_options = {'api_endpoint': f'{your_region}-dataproc.googleapis.com:443'})
That error indicates your project cannot use that region. However, I think the issue is in how you specify the Dataproc region as regions/europe-west1-b. Instead, please try europe-west1

Error when creating a pool from a custom image with azure python sdk

I'm trying to create a pool using a custom image I created from VM with azure python sdk. The location and resource group match.
Here's my code:
import azure.batch as batch
from azure.batch import BatchServiceClient
from azure.batch.batch_auth import SharedKeyCredentials
from azure.batch import models
account = 'mybatch'
key = 'Adgfdj1hhsdfqATc/K2fgxdfg/asYgKRP2pUdfglBce7mgmSBdfgdhC7f3Zdfgrcgkdgh/dfglA=='
batch_url = 'https://mybatch.westeurope.batch.azure.com'
creds = SharedKeyCredentials(account, key)
batch_client = BatchServiceClient(creds, base_url = batch_url)
pool_id_base = 'mypool'
idx = 1
pool_id = pool_id_base + str( idx )
while batch_client.pool.exists( pool_id ):
idx += 1
pool_id = pool_id_base + str( idx )
print( 'pool_id ' + pool_id )
sku_to_use = 'batch.node.ubuntu 18.04'
#
# image_ref_to_use = models.ImageReference(
# offer = 'UbuntuServer',
# publisher = 'Canonical',
# sku = '18.04-LTS',
# version = 'latest'
# )
image_ref_to_use = models.ImageReference(
virtual_machine_image_id = '/subscriptions/1834572sd-34sd409a-sdfb-sc345csdfesourceGroups/resource-group-1/providers/Microsoft.Compute/images/my-image-1'
)
vm_size = 'Standard_D3_v2'
vmc = models.VirtualMachineConfiguration(
image_reference = image_ref_to_use,
node_agent_sku_id = sku_to_use
)
new_pool = models.PoolAddParameter(
id = pool_id,
vm_size = vm_size,
virtual_machine_configuration = vmc,
target_dedicated_nodes = 1
)
batch_client.pool.add(new_pool)
According to the docs I should be able to use either virtual_machine_image_id other provide marketplace image parameters.
I can create a pool of standard marketplace images, but I get an error when I'm trying to use an id of my custom image.
Traceback (most recent call last): File "create_pool.py", line 60, in <module>
batch_client.pool.add(new_pool) File "/root/miniconda/lib/python3.6/site-packages/azure/batch/operations/pool_operations.py", line 312, in add
raise models.BatchErrorException(self._deserialize, response) azure.batch.models.batch_error.BatchErrorException: {'additional_properties': {}, 'lang': 'en-US', 'value': 'Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.\nRequestId:0dfdf9c1-edad-4b72-8e8f-f8dbcfd0abbdf\nTime:2018-12-06T10:51:21.9417222Z'}
How can I resolve this issue?
UPDATE
I tried to use ServicePrincipalCredentials with the following:
CLIENT_ID: I created a new application in Defaut Directiry -> Add registration and got it's Application ID.
SECRET: A created a key for the new application and used its value.
TENANT_ID: az account show in the cloud shell.
RESOURCE: Used 'https://batch.core.windows.net/'.
Updated my code like this:
from azure.common.credentials import ServicePrincipalCredentials
creds = ServicePrincipalCredentials(
client_id=CLIENT_ID,
secret=SECRET,
tenant=TENANT_ID,
resource=RESOURCE
)
And I get another error:
Keyring cache token has failed: No recommended backend was available. Install the keyrings.alt package if you want to use the non-recommended backends. See README.rst for details.
Traceback (most recent call last):
File "create_pool.py", line 41, in <module>
while batch_client.pool.exists( pool_id ):
File "/root/miniconda/lib/python3.6/site-packages/azure/batch/operations/pool_operations.py", line 624, in exists
raise models.BatchErrorException(self._deserialize, response)
azure.batch.models.batch_error.BatchErrorException: Operation returned an invalid status code 'Server failed to authorize the request.'
Try using Service Principal Credentials instead of the Shared Key Credentials
credentials = ServicePrincipalCredentials(
client_id=CLIENT_ID,
secret=SECRET,
tenant=TENANT_ID,
resource=RESOURCE
)
There seems to be an error with Shared Key Credentials.
Documentation Link: https://learn.microsoft.com/en-us/azure/batch/batch-aad-auth
Issue Link: https://github.com/Azure/azure-sdk-for-python/issues/1668
Note: Please remove your account details as it can be accessed by anyone. Replace the account name and key with ****.
Update
If the Service Principal Credentials are not working, try using the User credentials and see if that works.
from azure.common.credentials import UserPassCredentials
import azure.batch.batch_service_client as batch
credentials = UserPassCredentials(
azure_user,
azure_pass
)
batch_client = batch.BatchServiceClient(credentials, base_url = batch_url)

Resources