Airflow authentication with RBAC and Key cloak - authentication

I want to implement rbac based auth in airflow with keycloak. Can someone help me with it.
I have creaed the webserver.config file and I am using docker to up the airflow webserver.
from airflow.www_rbac.security import AirflowSecurityManager
from flask_appbuilder.security.manager import AUTH_OAUTH
import os
import json
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION_ROLE = "Admin"
OAUTH_PROVIDERS = [
{
'name': 'keycloak',
'icon': 'fa-user-circle',
'token_key': 'access_token',
'remote_app': {
'base_url': 'http://localhost:8180/auth/realms/airflow/protocol/openid-connect/',
'request_token_params': {
'scope': 'email profile'
},
'request_token_url': None,
'access_token_url': 'http://localhost:8180/auth/realms/airflow/protocol/openid-connect/token',
'authorize_url': 'http://localhost:8180/auth/realms/airflow/protocol/openid-connect/auth',
'consumer_secret': "98ec2e89-9902-4577-af8c-f607e34aa659"
}
}
]
I have also set the ariflow.cfg
rbac = True
authenticate = True
But still its not redirecting to the keycloak when the airflow is loaded.
I use :
docker build --rm --build-arg AIRFLOW_DEPS="datadog,dask" --build-arg PYTHON_DEPS="flask_oauthlib>=0.9" -t airflow .
and
docker run -d -p 8080:8080 airflow webserver
TO execute it.

I maybe coming late to this one and my answer may not work exactly as I'm using a different auth provider, however it's still OAuth2 and in a previous life I used Keycloak so my solution should also work there.
My answer makes use of authlib (At time of writing newer versions of airflow have switched. I am on 2.1.2)
I've raised a feature request against Flask-AppBuilder which Airflow uses as it's OAuth hander should really take care of things when the scope includes openid (you'd need to add this to your scopes)
From memory keycloak returns id_token along side the access_token and refresh_token and so this code simply decodes what has already been returned.
import os
import logging
import re
import base64
import yaml
from flask import session
from airflow.www.security import AirflowSecurityManager
from flask_appbuilder.security.manager import AUTH_OAUTH
basedir = os.path.abspath(os.path.dirname(__file__))
MY_PROVIDER = 'keycloak'
class customSecurityiManager(AirflowSecurityManager):
def oauth_user_info(self, provider, resp):
if provider == MY_PROVIDER:
log.debug("{0} response received : {1}".format(provider,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))
if not me.get("name"):
firstName = ""
lastName = ""
else:
firstName = me.get("name").split(' ')[0]
lastName = me.get("name").split(' ')[-1]
return {
"username": me.get("email"),
"email": me.get("email"),
"first_name": firstName,
"last_name": lastName,
"role_keys": me.get("groups", ['Guest'])
}
else:
return {}
log = logging.getLogger(__name__)
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Guest"
AUTH_ROLES_SYNC_AT_LOGIN = True
CSRF_ENABLED = True
AUTH_ROLES_MAPPING = {
"Airflow_Users": ["User"],
"Airflow_Admins": ["Admin"],
}
OAUTH_PROVIDERS = [
{
'name': MY_PROVIDER,
'icon': 'fa-circle-o',
'token_key': 'access_token',
'remote_app': {
'server_metadata_url': WELL_KNOWN_URL,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'client_kwargs': {
'scope': 'openid groups',
'token_endpoint_auth_method': 'client_secret_post'
},
'access_token_method': 'POST',
}
}
]
SECURITY_MANAGER_CLASS = customSecurityManager
Ironically the Azure provider already returns id_token and it's handled so my code makes use of that existing parsing
The code decodes id_token
Note you can turn on debug logging with the environmental variable AIRFLOW__LOGGING__FAB_LOGGING_LEVEL set to DEBUG.
If you switch on debug logs and see an entry like the following (note the id_token) you can probably use the code I've supplied.
DEBUG - OAUTH Authorized resp: {'access_token': '<redacted>', 'expires_in': 3600, 'id_token': '<redacted>', 'refresh_token': '<redacted>, 'scope': 'openid groups', 'token_type': 'Bearer', 'expires_at': <redacted - unix timestamp>}
The id_token is in 3 parts joined by a full stop . The middle part contains the user data and is simply base64 encoded

Related

#shopify/shopify-api nodejs - Set permanent access token for private app installed on only one store

The shopifyApi reference does not have a private app permanent access token property. I have a custom private app that is installed on only one store and I have the permanent access token so I don't need to oAuth every time I'm calling the REST API. Would be great to have a documented straight forward example of how to do this. The docs are hairy, IMO.
#shopify/shopify-api version: 6.1.0
Is the following close? I'm looking at the Session object and trying to understand how to load an offline session and set the Session with correct SessionParams.
import '#shopify/shopify-api/adapters/node'
import { shopifyApi, LATEST_API_VERSION, Session } from '#shopify/shopify-api'
const shopify = shopifyApi({
apiKey: 'myprivateappkey',
apiSecretKey: 'myprivateappsecret',
apiVersion: LATEST_API_VERSION,
isPrivateApp: true,
scopes: ['read_products'],
isEmbeddedApp: false,
hostName: 'shop.myshopify.com',
})
const sessionId = shopify.session.getOfflineId('shop.myshopify.com')
const session = new Session({
id: sessionId,
shop: 'shop.myshopify.com',
state: 'state',
isOnline: false,
accessToken: 'permanentAccessToken',
})
const client = new shopify.clients.Rest({ session: session })
On older v5 version of shopify-api, with two lines I could access any REST API, but I see this is now deprecated, So I'm trying to unravel offline sessions, but it doesn't make sense to provide the api key and api secret and then create a session using the permanent access token.
import Shopify from '#shopify/shopify-api'
const client = new Shopify.Clients.Rest(
'mystore.myshopify.com',
'permanentAccessTokenString',
)
See answer from Shopify here.
When creating the client in a private app, a dummy session can be created like so
const session = new Session({
id: 'not-a-real-session-id',
shop: 'shop.myshopify.com,
state: 'state',
isOnline: false,
});
const client = new shopify.clients.Rest({ session: session })
When config.isPrivateApp is set to true only the shop property is used by the client - the other three (id, state, isOnline) properties are ignored (but are required when creating a Session object).
config.apiSecretKey is used as the access token, and is read directly from the config (no need to set the accessToken property of the dummy session as it will be ignored).
Essentially, config.apiSecretKey is the permanent access token for a private app.
The scopes can be omitted, as it defaults to an empty scopes array internally anyway and (from a quick search through the library code) is only used when doing OAuth, validating sessions, etc., which won't apply to private apps.
As for the apiKey, while it's mostly used as part of the OAuth process, it is also used in a few other places (e.g., shopify.auth.getEmbeddedAppUrl()), so I'd recommend setting the apiKey to be that of your private app.
However, in my testing, scopes, even when isPrivateApp, are currently required. If you leave the array empty for scopes it will have a config error.
Also, my shopifyApi config mounts rest resources so that when you're in Shopify REST docs on the nodejs tab, you can easily use the REST resources examples on the nodejs to make calls, rather than creating a REST client and use standard post, etc.
Complete code to setup a private app connection to Shopify:
import '#shopify/shopify-api/adapters/node'
import { shopifyApi, LATEST_API_VERSION, Session } from '#shopify/shopify-api'
import { restResources } from '#shopify/shopify-api/rest/admin/2023-01'
const debug = process.env.FUNCTIONS_EMULATOR === 'true'
const shopify = shopifyApi({
apiKey: 'myprivateAppApiKey',
apiSecretKey: 'myPermanentAccessToken',
apiVersion: LATEST_API_VERSION,
isPrivateApp: true,
scopes: [
'read_customers',
'write_customers',
'read_fulfillments',
'write_fulfillments',
'read_inventory',
'write_inventory',
'write_order_edits',
'read_order_edits',
'write_orders',
'read_orders',
'write_products',
'read_products',
],
isEmbeddedApp: false,
hostName: debug ? '127.0.0.1:5001' : 'shop.myshopify.com',
// Mount REST resources.
restResources,
})
// Create a sanitized "fake" sessionId. E.g.
// "offline_my.myshopify.com".
const sessionId = shopify.session.getOfflineId('shop.myshopify.com')
const session = new Session({
id: sessionId,
shop: 'shop.myshopify.com,
state: 'state',
isOnline: false,
})
// Use mounted REST resources to make calls.
const transactions = await shopify.rest.Transaction.all({
session,
order_id: 123456789,
})
// Alternatively, if not using mounted REST resources
// you could create a standard REST client.
const client = new shopify.clients.Rest({ session })

Extracting custom objects from HttpContext

Context
I am rewriting an ASP.NET Core application from being ran on lambda to run on an ECS Container. Lambda supports the claims injected from Cognito Authorizer out of the box, but Kestrel doesn't.
API requests are coming in through API Gateway, where a Cognito User Pool authorizer is validating the OAuth2 tokens and enriching the claims from the token to the httpContext.
Originally the app was running on lambda where the entry point was inheriting Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction, which extracts those claims and adds them to Request.HttpContext.User.Claims.
Kestrel of course doesn't support that and AWS ASPNET Cognito Identity Provider seems to be meant for performing the same things that the authorizer is doing.
Solution?
So I got the idea that maybe I can add some custom code to extract it. The HTTP request injected into lambda looks like this, so I expect it should be the same when it's proxied into ECS
{
"resource": "/{proxy+}",
"path": "/api/authtest",
"httpMethod": "GET",
"headers": {
<...>
},
"queryStringParameters": null,
"pathParameters": {
"proxy": "api/authtest"
},
"requestContext": {
"resourceId": "8gffya",
"authorizer": {
"cognito:groups": "Admin",
"phone_number_verified": "true",
"cognito:username": "normj",
"aud": "3mushfc8sgm8uoacvif5vhkt49",
"event_id": "75760f58-f984-11e7-8d4a-2389efc50d68",
"token_use": "id",
"auth_time": "1515973296",
"you_are_special": "true"
}
<...>
}
Is it possible, and how could I go about it to add all the key / value pairs from requestContext.authorizer to Request.HttpContext.User.Claims?
I found a different solution for this.
Instead of trying to modify the HttpContext I map the authorizer output to request headers in the API Gateway integration. Downside of this is that each claim needs to be hardcoded as it doesn't seem to be possible to iterate over them.
Example terraform
resource "aws_api_gateway_integration" "integration" {
rest_api_id = "${var.aws_apigateway-id}"
resource_id = "${aws_api_gateway_resource.proxyresource.id}"
http_method = "${aws_api_gateway_method.method.http_method}"
integration_http_method = "ANY"
type = "HTTP_PROXY"
uri = "http://${aws_lb.nlb.dns_name}/{proxy}"
connection_type = "VPC_LINK"
connection_id = "${aws_api_gateway_vpc_link.is_vpc_link.id}"
request_parameters = {
"integration.request.path.proxy" = "method.request.path.proxy"
"integration.request.header.Authorizer-ResourceId" = "context.authorizer.resourceId"
"integration.request.header.Authorizer-ResourceName" = "context.authorizer.resourceName"
"integration.request.header.Authorizer-Scopes" = "context.authorizer.scopes"
"integration.request.header.Authorizer-TokenType" = "context.authorizer.tokenType"
}
}

'FirebaseApplication' object has no attribute 'Authentication'

I'm using the following code to fetch data from Firebase on Raspberry pi but it shows following error. However i'm able to fetch data without authentication.
from firebase.firebase import FirebaseApplication
from firebase.firebase import FirebaseAuthentication
firebase = firebase.FirebaseApplication('https://myapp.firebaseio.com/',
authentication =None)
authentication = firebase.Authentication('secretkey',
'prateekrai266#gmail.com', extra={'id': 123})
firebase.authentication = authentication
print (authentication.extra)
user = authentication.get_user()
print (user.firebase_auth_token)
result = firebase.get('/messages', None)
it shows following error
Traceback (most recent call last):
File "/home/pi/code/dataauth.py", line 7, in authentication =
firebase.Authentication('secretkey', 'prateekrai266#gmail.com',
extra={'id': 123}) AttributeError: 'FirebaseApplication' object has no
attribute 'Authentication'
I'm able to fetch data without authentication i.e. setting rules to true, by following code
from firebase.firebase import FirebaseApplication
firebase = firebase.FirebaseApplication('https://myapp.firebaseio.com/',
None)
result = firebase.get('/messages', None)
I presume you are using python-firebase which is a bit deprecated and not updated for long time. I've tried to fix your problem, found solution for your issue, but problem with authenticating still occurs.
Code:
from firebase.firebase import FirebaseApplication
from firebase.firebase import FirebaseAuthentication
DSN = config['databaseURL'] # 'https://myapp.firebaseio.com/'
SECRET = config['userPass'] # 'secretkey'
EMAIL =config['userEmail'] # 'prateekrai266#gmail.com'
authentication = FirebaseAuthentication(SECRET,EMAIL, True, True)
firebase = FirebaseApplication(DSN, authentication)
firebase.get('/messages', None)
I would suggest to move into pyrebase which is more updated and as I've just checked works.
https://github.com/thisbejim/Pyrebase
Code that works for me:
import pyrebase
config = {
'apiKey': "YYY",
'authDomain': "XXXXXXXX.firebaseapp.com",
'databaseURL': "https://XXXXXXXX.firebaseio.com",
'projectId': "XXXXXXXX",
'storageBucket': "XXXXXXXX.appspot.com",
'messagingSenderId': "ZZZ",
}
firebase = pyrebase.initialize_app(config)
userEmail = 'youremailadresthatyouaddedtofirebasedatabaseauthenticatedusers'
userPass = 'yourpasswordsetupforthisemail'
auth = firebase.auth()
# Log the user in
user = auth.sign_in_with_email_and_password(userEmail, userPass)
# Get a reference to the database service
db = firebase.database()
# data to save
data = {
"name": "Mortimer 'Morty' Smith"
}
# Pass the user's idToken to the push method
results = db.child("test").push(data, user['idToken'])
print results
results = db.child("test").get(user['idToken'])
print results.val()

How to create a jenkins credentials via API?

Does anybody know how to create a new jenkins (2.8) credentials (f.e for a git access) via API or POST request in Jenkins? I have tried to use this code (from another stackoverflow topic), but it does nothing:
import json
import requests
def main():
data = {
'credentials': {
'scope': "GLOBAL",
'username': "jenkins",
'privateKeySource': {
'privateKey': "-----BEGIN RSA PRIVATE KEY-----\nX\n-----END RSA PRIVATE KEY-----",
'stapler-class': "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource"
},
'stapler-class': "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey"
}
}
payload = {
'json': json.dumps(data),
'Submit': "OK",
}
r = requests.post("http://%s:%d/credential-store/domain/_/createCredentials" % (localhost, 8080), data=payload)
if r.status_code != requests.codes.ok:
print r.text
I did it this way:
java -jar /tmp/jenkins-cli.jar -s http://localhost:8080/ \
groovy /tmp/credentials.groovy id username password
credentials.groovy
import jenkins.model.*
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.common.*
import com.cloudbees.plugins.credentials.domains.*
import com.cloudbees.plugins.credentials.impl.*
domain = Domain.global()
store = Jenkins.instance.getExtensionList('com.cloudbees.plugins.credentials.SystemCredentialsProvider')[0].getStore()
usernameAndPassword = new UsernamePasswordCredentialsImpl(
CredentialsScope.GLOBAL,
args[0],
"",
args[1],
args[2]
)
store.addCredentials(domain, usernameAndPassword)
I ran into the same issue and after a bit of digging/testing it seems you need to change this
/credential-store/domain/_/createCredentials
to this
/credentials/store/system/domain/_/createCredentials
It's doesn't work: /credential-store/domain/_/api/json
You have to use this url: /credentials/store/system/domain/_/api/json

OAuth2Decorator: Using developer's token to run API calls for user

For the "normal" oauth2 dance, I get to specify the user and get a corresponding token.
This allows me to make API calls masquerading as that user, i.e. on his behalf.
It can also allow the user to make calls masquerading as me.
A use case is bigquery where I don't have to grant table access to the user and I can specify my own preferred level of control.
Using the simplified OAuth2Decorator, I don't seem to have this option.
Am I right to say that?
Or is there a work-around?
In general, what is the best practice? To use the proper oauth (comprising of Flow, Credentials and Storage)? Or to use OAuth2Decorator.
Thank you very much.
You can certainly use an OAuth2Decorator
Here is an example:
main.py
import bqclient
import httplib2
import os
from django.utils import simplejson as json
from google.appengine.api import memcache
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from oauth2client.appengine import oauth2decorator_from_clientsecrets
PROJECT_ID = "xxxxxxxxxxx"
DATASET = "your_dataset"
QUERY = "select columns from dataset.table"
CLIENT_SECRETS = os.path.join(os.path.dirname(__file__),'client_secrets.json')
http = httplib2.Http(memcache)
decorator = oauth2decorator_from_clientsecrets(CLIENT_SECRETS,
'https://www.googleapis.com/auth/bigquery')
bq = bqclient.BigQueryClient(http, decorator)
class MainHandler(webapp.RequestHandler):
#decorator.oauth_required
def get(self):
data = {'data': json.dumps(bq.Query(QUERY, PROJECT_ID))}
template = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(render(template, data))
application = webapp.WSGIApplication([('/', MainHandler),], debug=True)
def main():
run_wsgi_app(application)
if __name__ == '__main__':
main()
bqclient.py that gets imported in your main.py which handles BigQuery actions
from apiclient.discovery import build
class BigQueryClient(object):
def __init__(self, http, decorator):
"""Creates the BigQuery client connection"""
self.service = build('bigquery', 'v2', http=http)
self.decorator = decorator
def Query(self, query, project, timeout_ms=10):
query_config = {
'query': query,
'timeoutMs': timeout_ms
}
decorated = self.decorator.http()
queryReply = (self.service.jobs()
.query(projectId=project, body=query_config)
.execute(decorated))
jobReference=queryReply['jobReference']
while(not queryReply['jobComplete']):
queryReply = self.service.jobs().getQueryResults(
projectId=jobReference['projectId'],
jobId=jobReference['jobId'],
timeoutMs=timeout_ms).execute(decorated)
return queryReply
where all your authentication details are kept in a json file client_secrets.json
{
"web": {
"client_id": "xxxxxxxxxxxxxxx",
"client_secret": "xxxxxxxxxxxxxxx",
"redirect_uris": ["http://localhost:8080/oauth2callback"],
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token"
}
}
finally, don't forget to add these lines to your app.yaml:
- url: /oauth2callback
script: oauth2client/appengine.py
Hope that helps.
I am not sure I completely understand the use case, but if you are creating an application for others to use without their having to authorize access based on their own credentials, I would recommend using App Engine service accounts.
An example of this type of auth flow is described in the App Engine service accounts + Prediction API article.
Also, see this part and this part of the App Engine Datastore to BigQuery codelab, which also uses this authorization method.
The code might look something like this:
import httplib2
# Available in the google-api-python-client lib
from apiclient.discovery import build
from oauth2client.appengine import AppAssertionCredentials
# BigQuery Scope
SCOPE = 'https://www.googleapis.com/auth/bigquery'
# Instantiate and authorize a BigQuery API client
credentials = AppAssertionCredentials(scope=SCOPE)
http = credentials.authorize(httplib2.Http())
bigquery_service = build("bigquery", "v2", http=http)
# Make some calls to the API
jobs = bigquery_service.jobs()
result = jobs.insert(projectId='some_project_id',body='etc, etc')