Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple tenants #94

Open
barrachri opened this issue Jul 10, 2022 · 7 comments
Open

Multiple tenants #94

barrachri opened this issue Jul 10, 2022 · 7 comments

Comments

@barrachri
Copy link

barrachri commented Jul 10, 2022

Hi there, thanks for making it easier to connect with xero!

I am trying to understand how to leverage xero-python to access multiple organizations.

Let's consider that I have 2 users, who are trying to access different organizations/tenants.

Now all the examples I saw cover using a flask session to store the token. But that solution won't work in a celery task or background job.

I was checking the node client, and it makes super easy to set the token:

import { XeroClient } from 'xero-node';

const xero = new XeroClient({
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET',
  redirectUris: [`http://localhost/:${port}/callback`],
  scopes: 'openid profile email accounting.transactions offline_access'.split(" ")
});

await xero.initialize();

const tokenSet = getTokenSetFromDatabase(userId); // example function name

await xero.setTokenSet(tokenSet); // Set the token inside the xero client

if(tokenSet.expired()){
  const validTokenSet = await xero.refreshToken();
  // save the new tokenset
}

while with the python client you are forced to use obtain_xero_oauth2_token and store_xero_oauth2_token as a sort of global cache:

# configure token persistence and exchange point between app session and xero-python
@api_client.oauth2_token_getter
def obtain_xero_oauth2_token():
    return session.get("token")

@api_client.oauth2_token_saver
def store_xero_oauth2_token(token):
    session["token"] = token
    session.modified = True

# get existing token set
token_set = get_token_set_from_database(user_id); // example function name

# set token set to the api client
store_xero_oauth2_token(token_set)

# refresh token set on the api client
api_client.refresh_oauth2_token()

any idea on how to avoid this?

@RettBehrens
Copy link
Contributor

Hi @barrachri I'm not familiar with celery but if you can share a hello world project that I can run locally, I'll give it a go. We use the session to store the token in our sample apps for the sake of simplicity but it's not required. You just need to configure the exchange point using the decorators to get and set the token on the api client, but you could be getting/saving the token from/to a db instead

@RettBehrens RettBehrens self-assigned this Aug 16, 2022
@barrachri
Copy link
Author

barrachri commented Aug 17, 2022

Hi @RettBehrens, thanks for your answer.

how would you implement something like this?

# Get invoices for different customers (different tenants)

api_client = ApiClient(
    Configuration(
        debug=app.config["DEBUG"],
        oauth2_token=OAuth2Token(
            client_id=app.config["CLIENT_ID"], client_secret=app.config["CLIENT_SECRET"]
        ),
    ),
    pool_threads=1,
)

# different tenants, with different access token
tenants = [xero_tenant_id_1, xero_tenant_id_2]
invoices = []
  
accounting_api = AccountingApi(api_client)
for tenant in tenants:
    invoices.append(accounting_api.get_invoices(tenant))

From what I understood the only way is this one

api_client = ApiClient(
    Configuration(
        debug=app.config["DEBUG"],
        oauth2_token=OAuth2Token(
            client_id=app.config["CLIENT_ID"], client_secret=app.config["CLIENT_SECRET"]
        ),
    ),
    pool_threads=1,
)

CURRENT_TOKEN = None

@api_client.oauth2_token_getter
def obtain_xero_oauth2_token():
    global CURRENT_TOKEN
    return CURRENT_TOKEN

@api_client.oauth2_token_saver
def store_xero_oauth2_token(token):
    CURRENT_TOKEN = token

# different tenants, with different access token
tenants = [xero_tenant_id_1, xero_tenant_id_2]
invoices = []
  
accounting_api = AccountingApi(api_client)
for tenant in tenants:
    token_set = get_token_from_somewhere()
    store_xero_oauth2_token(token_set)
    invoices.append(accounting_api.get_invoices(tenant))

If that's the only way, I think it would be nice to add a set_token like the js.

api_client = ApiClient(
    Configuration(
        debug=app.config["DEBUG"],
        oauth2_token=OAuth2Token(
            client_id=app.config["CLIENT_ID"], client_secret=app.config["CLIENT_SECRET"]
        ),
    ),
    pool_threads=1,
)

# different tenants, with different access token
tenants = [xero_tenant_id_1, xero_tenant_id_2]
invoices = []
  
accounting_api = AccountingApi(api_client)
for tenant in tenants:
    token_set = get_token_from_somewhere()
    api_client.set_token(token_set)
    invoices.append(accounting_api.get_invoices(tenant))

@RettBehrens
Copy link
Contributor

Hey @barrachri an update on this. Your understanding was correct - currently you'd have to set the token as a global.

Currently the team is focused on squashing bugs and keeping parity with the public API. Since this is more of a feature/enhancement request, it's going to be on the back burner, however, if you want to submit a PR implementing the feature, we'd be happy to review and merge and arrange some Xero swag for your contribution 🙂

@joe-niland
Copy link

joe-niland commented Jan 20, 2023

You've probably solved this by now but the ApiClient constructor allows you to pass in functions for token get/set like this:

api_client = ApiClient(
    Configuration(
        debug=app.config["DEBUG"],
        oauth2_token=OAuth2Token(
            client_id=app.config["CLIENT_ID"], client_secret=app.config["CLIENT_SECRET"]
        ),
    ),
    oauth2_token_saver=update_token,
    oauth2_token_getter=fetch_token,
    pool_threads=1,
)

In these you can store/retrieve the token any way you like.

@barrachri
Copy link
Author

Thanks @joe-niland!

@jaredthecoder
Copy link

jaredthecoder commented Apr 26, 2023

@joe-niland this is what we have done to handle this the past couple years. Confirming for others. :)

@joe-niland
Copy link

joe-niland commented Apr 26, 2023

@rdemarco-xero I think this issue can be closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants