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

Feature: Airtable Analyzer for Personal Access Tokens #3941

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

nabeelalam
Copy link
Contributor

@nabeelalam nabeelalam commented Feb 26, 2025

Description:

Airtable PAT Analyzer

image

Checklist:

  • Tests passing (make test-community)?
  • Lint passing (make lint this requires golangci-lint)?

@nabeelalam nabeelalam changed the title Feature: Airtable Analyzer for Personal Access Tokens [WIP] Feature: Airtable Analyzer for Personal Access Tokens Feb 27, 2025
@nabeelalam nabeelalam marked this pull request as ready for review February 28, 2025 15:46
@nabeelalam nabeelalam requested review from a team as code owners February 28, 2025 15:46
@nabeelalam nabeelalam changed the title [WIP] Feature: Airtable Analyzer for Personal Access Tokens Feature: Airtable Analyzer for Personal Access Tokens Feb 28, 2025
Copy link
Collaborator

@ahrav ahrav left a comment

Choose a reason for hiding this comment

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

Great job @nabeelalam! Left a few small suggestion and questions, but otherwise looks great.

return nil, err
}

scopeStatusMap[common.PermissionStrings[common.UserEmailRead]] = userInfo.Email != nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: Is the idea here that if the response from the FetchAirtableUserInfo API contains an email, they have the user.email:read scope?

scopeStatusMap[common.PermissionStrings[common.UserEmailRead]] = userInfo.Email != nil

var basesInfo *common.AirtableBases
if granted, err := determineScope(token, common.SchemaBasesRead, nil); granted {
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: The way determineScope is written, I’m not sure there’s a possible code path where granted == true and err != nil. As a result, the error check right below this will never be triggered.

Additionally, this approach seems to be swallowing the error—was that intentional? If so, could we add a comment explaining why that design is acceptable? I bring this up because, throughout the rest of the implementation, we return an error if one occurs during an API call. If this is an exception, it should be explicitly documented.

Alternatively, we could remove the inline granted check and handle the error first.
Ex:

granted, err := determineScope(token, common.SchemaBasesRead, nil)
if err != nil {
    return nil, err
}
if granted {
    ...
}

if resp.StatusCode == http.StatusOK {
scopeStatusMap[scopeString] = true
return true, nil
} else if endpoint.ExpectedErrorResponse != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: Since the if block returns, we don’t need an else if. We can de-indent this and use a regular if, which improves readability by following the line of sight coding principle. ([Reference](https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88))

ex:

if resp.StatusCode == http.StatusOK {
    scopeStatusMap[scopeString] = true
    return true, nil
}
if endpoint.ExpectedErrorResponse != nil {
..
}

scopeStatusMap[scopeString] = true
return true, nil
} else if endpoint.ExpectedErrorResponse != nil {
var result map[string]interface{}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: interface{} -> any

return false, err
}

if errorInfo, ok := result["error"].(map[string]interface{}); ok {
Copy link
Collaborator

Choose a reason for hiding this comment

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

ditto

}
}

func determineScope(token string, scope common.Permission, ids map[string]string) (bool, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: For consistency and better readability, can we rename scope to permission or perm? Since it's later used as an argument for getEndpointByPermission, this would make the code clearer.

}

func determineScopes(token string, basesInfo *common.AirtableBases) error {
if basesInfo != nil && len(basesInfo.Bases) > 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: Similar to my earlier "line of sight" comment, we can invert the logic of this if statement to return early, allowing us to de-indent the following for loop for better readability.

Ex:

if basesInfo == nil || len(basesInfo.Bases) == 0 {
    return nil
}

for _ base := range baseInfo.Bases {
    ...
}

tableScopesDetermined = true
}

if granted, err := determineScope(token, common.DataRecordsRead, ids); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: We can also remove the if else and just use an if at the same indentation level, since the if block already returns.

Ex:

granted, err := determineScope(token, common.DataRecordsRead, ids)
if err != nil {
	return err
}
if !granted {
    continue
}
...

return err
} else if granted {
// Verifying scopes that require an existing record and record read permission
records, err := fetchAirtableRecords(token, base.ID, table.ID)
Copy link
Collaborator

Choose a reason for hiding this comment

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

question/suggestion: It looks like we might be again swallowing the error? If this is also done intentionally let's add a comment explaining why. We can make this a little easier to reason about if we check the err and handle it first. We then don't even need to check the len(records) we can simply range over them.

Ex:

records, err := fetchAirtableRecords(token, base.ID, table.ID)
if err != nil {
    return err // or continue
}
for _, record := range records {
    ids["recordID"] = record.ID
    _, err = determineScope(token, common.DataRecordcommentsRead, ids)
    if err != nil {
	    return err
    }
    break
}

return nil
}

var Endpoints = map[EndpointName]Endpoint{
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a nice pattern! One small, optional suggestion: consider constructing this in an init() function and making it private with a public getter.

It’s not a big deal, but this approach prevents unintended modifications by external clients since it wouldn’t be directly exported. I don’t think it's a major concern here, but just something to consider! Totally optional. 😊

Ex:

var endpoints map[EndpointName]Endpoint

func init() {
	endpoints = map[EndpointName]Endpoint{
		GetUserInfoEndpoint: {
			URL:    "https://api.airtable.com/v0/meta/whoami",
			Method: "GET",
		},
		// ... existing code ...
	}
}

// GetEndpoint returns the endpoint for the given name and whether it exists
func GetEndpoint(name EndpointName) (Endpoint, bool) {
	endpoint, exists := endpoints[name]
	return endpoint, exists
}

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

Successfully merging this pull request may close these issues.

2 participants