Skip to content

Multi-tenancy changes to shared Auth API requests #539

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

Merged
merged 4 commits into from
Jun 3, 2019

Conversation

bojeil-google
Copy link
Contributor

Defines BaseFirebaseAuthRequestHandler class for sending Auth requests related to user management APIs and SAML/OIDC config mgmt APIs, link generation, etc.

Defines FirebaseAuthRequestHandler which extends the base class for project level only calls and which will also be extended to include tenant mgmt APIs.
Defines FirebaseTenantRequestHandler which extends the base class for tenant level only calls.

Unit tests have been modified to run tests on both subclasses. 99% of the unit test logic is the same. It has been tweaked to accept a common interface handler. 2 tests are run for each. One for FirebaseAuthRequestHandler and another for FirebaseTenantRequestHandler.

…s related to user management APIs and SAML/OIDC config mgmt APIs, link generation, etc.

Defines FirebaseAuthRequestHandler which extends the base class for project level only calls and which will also be extended to include tenant mgmt APIs.
Defines FirebaseTenantRequestHandler which extends the base class for tenant level only calls.

Unit tests have been modified to run tests on both subclasses.
@@ -205,6 +236,7 @@ function validateCreateEditRequest(request: any, uploadAccountRequest: boolean =
phoneNumber: true,
customAttributes: true,
validSince: true,
tenantId: true,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the boolean value mean here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the list of allowed parameters to pass to the backend for create/edit requests (it's a quick way to generate a Set). If the developer passes other parameters, they will be either ignored or an error thrown.

@@ -217,6 +249,10 @@ function validateCreateEditRequest(request: any, uploadAccountRequest: boolean =
delete request[key];
}
}
if (typeof request.tenantId !== 'undefined' &&
!validator.isNonEmptyString(request.tenantId)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the check be the mismatch between tenantId in the request and the tenantId in the user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I am only doing basic validation (ensure tenant ID is non-empty string when provided). I am leaving the check to the backend to enforce the match. I wasn't sure that uploadAccount allows uploading users with different tenants. That is why I am not checking mismatch. For now, I will rely on backend check. I added the mismatch tenant ID backend error catching I think i can followup with a mismatch check client side.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There probably won't be a mismatch tenant ID backend error. For UploadAccount you can always rely on backend returning error messages in the response for specific failed users. For the other endpoints like SetAccountInfo and SignupNewUsers, the backend won't be able to return the error since it only receives one tenant_id from the request as I mentioned in a separate email thread. Adding the client side check in a follow-up change sounds good to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Yeah I will add the mismatch check in a follow up.

Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are somewhat complicated to review. I trust it's just the existing test cases configured to run with both handler implementations.

Just a couple of suggestions to improve readability and the design.

constructor(protected projectId: string, protected version: string, protected tenantId: string) {
super(projectId, version);
// Inject tenant path in URL.
this.urlFormat = this.urlFormat.replace(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like urlFormat in the parent class is really just a constant. Can we just define a constant for this as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

private readonly httpClient: AuthorizedHttpClient;
private readonly authUrlBuilder: AuthResourceUrlBuilder;
private readonly projectConfigUrlBuilder: AuthResourceUrlBuilder;
export class BaseFirebaseAuthRequestHandler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this class abstract and rename it to AbstractAuthRequestHandler?

getAuthUrlBuilder() and getProjectConfigUrlBuilder() could be abstract methods of the class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* tenant management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines
* additional tenant management related APIs.
*/
export class FirebaseAuthRequestHandler extends BaseFirebaseAuthRequestHandler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestions for names would be AuthRequestHandler and TenantAwareAuthRequestHandler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor Author

@bojeil-google bojeil-google left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the test changes, this is basically what I am doing:

Old test:

describe('FirebaseAuthRequestHandler', () => {
  describe('getAccountInfoByUid', () => {
    const path = '/v1/projects/project_id/accounts:lookup';
    const method = 'POST';
    it('should be fulfilled given a valid localId', () => {
      const expectedResult = utils.responseFrom({
        users : [
          {localId: 'uid'},
        ],
      });
      const data = {localId: ['uid']};
      const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
      stubs.push(stub);

      const requestHandler = new FirebaseAuthRequestHandler(mockApp);
      return requestHandler.getAccountInfoByUid('uid')
        .then((result) => {
          expect(result).to.deep.equal(expectedResult.data);
          expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data));
        });
    });
  });
});

Changed test:

const AUTH_REQUEST_HANDLER_TESTS: HandlerTest[] = [
  {
    name: 'FirebaseAuthRequestHandler',
    init: (app: FirebaseApp) => {
      return new AuthRequestHandler(app);
    },
    path: (version: string, api: string, projectId: string) => {
      return `/${version}/projects/${projectId}${api}`;
    },
    supportsTenantManagement: true,
  },
  {
    name: 'FirebaseTenantRequestHandler',
    init: (app: FirebaseApp) => {
      return new TenantAwareAuthRequestHandler(app, TENANT_ID);
    },
    path: (version: string, api: string, projectId: string) => {
      return `/${version}/projects/${projectId}/tenants/${TENANT_ID}${api}`;
    },
    supportsTenantManagement: false,
  },
];

const TENANT_ID = 'tenantId';
AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => {
  describe(handler.name, () => {
    describe('getAccountInfoByUid', () => {
      const path = handler.path('v1', '/accounts:lookup', 'project_id');
      const method = 'POST';
      it('should be fulfilled given a valid localId', () => {
        const expectedResult = utils.responseFrom({
          users : [
            {localId: 'uid'},
          ],
        });
        const data = {localId: ['uid']};
        const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
        stubs.push(stub);

        const requestHandler = handler.init(mockApp);
        return requestHandler.getAccountInfoByUid('uid')
          .then((result) => {
            expect(result).to.deep.equal(expectedResult.data);
            expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data));
          });
      });
  });
});

The main changes is that I introduced AUTH_REQUEST_HANDLER_TESTS to test both subclasses.
Each test I get the associated handler and get path to check in request by calling handler.path().
In addition, instead of initializing const requestHandler = new AuthRequestHandler(mockApp) manually, I use const requestHandler = handler.init(mockApp).

The logic is the same. However, GitHub's diff gets super confused from these changes. Note that Visual Studio Code does a great job catching the actual changes.

constructor(protected projectId: string, protected version: string, protected tenantId: string) {
super(projectId, version);
// Inject tenant path in URL.
this.urlFormat = this.urlFormat.replace(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -205,6 +236,7 @@ function validateCreateEditRequest(request: any, uploadAccountRequest: boolean =
phoneNumber: true,
customAttributes: true,
validSince: true,
tenantId: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the list of allowed parameters to pass to the backend for create/edit requests (it's a quick way to generate a Set). If the developer passes other parameters, they will be either ignored or an error thrown.

@@ -217,6 +249,10 @@ function validateCreateEditRequest(request: any, uploadAccountRequest: boolean =
delete request[key];
}
}
if (typeof request.tenantId !== 'undefined' &&
!validator.isNonEmptyString(request.tenantId)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I am only doing basic validation (ensure tenant ID is non-empty string when provided). I am leaving the check to the backend to enforce the match. I wasn't sure that uploadAccount allows uploading users with different tenants. That is why I am not checking mismatch. For now, I will rely on backend check. I added the mismatch tenant ID backend error catching I think i can followup with a mismatch check client side.

* tenant management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines
* additional tenant management related APIs.
*/
export class FirebaseAuthRequestHandler extends BaseFirebaseAuthRequestHandler {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

private readonly httpClient: AuthorizedHttpClient;
private readonly authUrlBuilder: AuthResourceUrlBuilder;
private readonly projectConfigUrlBuilder: AuthResourceUrlBuilder;
export class BaseFirebaseAuthRequestHandler {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. Just one suggestion to keep couple of attributes as readonly.

protected readonly projectId: string;
protected readonly httpClient: AuthorizedHttpClient;
protected authUrlBuilder: AuthResourceUrlBuilder;
protected projectConfigUrlBuilder: AuthResourceUrlBuilder;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can keep these 2 properties readonly. Lets initialize them in the constructor of the abstract class, by calling the abstract methods:

this.authUrlBuilder = this.getAuthUrlBuilder();
this.projectConfigUrlBuilder = this.getProjectConfigUrlBuilder();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can call the abstract methods newAuthUrlBuilder() and newProjectConfigUrlBuilder() if that sounds better.

/**
* @return {AuthResourceUrlBuilder} The Auth user management resource URL builder.
*/
protected getAuthUrlBuilder(): AuthResourceUrlBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we make the authUrlBuilder readonly, these method implementation will become simplified:

protected getAuthUrlBuilder() {
  return new AuthResourceUrlBuilder(this.projectId, 'v1');
}

Copy link
Contributor

@hiranya911 hiranya911 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@hiranya911 hiranya911 merged commit 5c7d44d into auth-multi-tenancy Jun 3, 2019
@bojeil-google bojeil-google deleted the temp-multi-tenancy2 branch June 4, 2019 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants