Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

ADO: Implement user permissions sync#48209

Merged
indradhanush merged 28 commits into
mainfrom
ig/ado-user-perms-backup
Mar 2, 2023
Merged

ADO: Implement user permissions sync#48209
indradhanush merged 28 commits into
mainfrom
ig/ado-user-perms-backup

Conversation

@indradhanush

@indradhanush indradhanush commented Feb 24, 2023

Copy link
Copy Markdown
Contributor

Originally created #48041 for early feedback. I've addressed those and created a new PR because it diverged a lot from the original branch.

Major progress on #47165. There's a little bit of follow up work required which would add more code to this PR - the implementation of ValidationCheck on the authz provider. I will follow up with a new PR, but the absence of it does not prevent a good configuration of ADO from working.

Note to reviewers

Not necessarily a big PR feature wise, ~500 lines is test code. But this does touch multiple modules that would make it hard to piece together for anyone not too familiar with the permissions syncing code. I've broken it down into sequential commits that should make sense when viewed in isolation for the most part. Each commit has a description describing the context and what to expect next. So it might be easier to view each commit at a time and then the PR as a whole.

Also note the usage of authn and authz in the commit messages.

Test plan

@cla-bot cla-bot Bot added the cla-signed label Feb 24, 2023
@indradhanush indradhanush force-pushed the ig/ado-user-perms-backup branch 2 times, most recently from 95bf1c9 to d7061d1 Compare February 28, 2023 06:02
The Azure Dev Ops API expects some custom key-value pairs in the POST
body for refreshing the token. This commit adds support for custom
key-value pairs so that the azuredevops authz provider can pass them in
the token refresher func.
This will be needed to create a new azuredevops authz provider from the
site config.

Just a new type has been added here. The other changes are alphabetical
sorting of the other providers for future readers.
@indradhanush indradhanush force-pushed the ig/ado-user-perms-backup branch 2 times, most recently from dc20e83 to b892b26 Compare February 28, 2023 10:35
In this commit we add some helpers and modify the client to support
refreshing expired / close to expiry on the fly as part of the
request.Do call path.

We also rename azureDevOpsServicesURL to AZURE_DEV_OPS_API_URL because
we will need it outside this package.

Also we export HTTPError and modify it's Error() method to drop the body
from the response because the body is raw HTML which pollutes the logs
and error strings.

Finally, we also add a helper GetRedirectURL that will be used in both
ADO auth and authz providers.
When creating an external account we want to use AZURE_DEV_OPS_API_URL
as the ServiceID when storing an entry in the user_external_accounts
table because this is the URL used in the external service.

We're not able to use s.ServiceID in the assignment because it would
incorrectly assign the value of VISUAL_STUDIO_APP_URL to the service ID.
The result of this would then be:

During the permission sync, the ADO authz provider created uses
AZURE_DEV_OPS_API_URL as the service ID. But the entry in the
user_external_accounts_table would have the value of
VISUAL_STUDIO_APP_URL as the ServiceID. Leading to a mismatch and the
permissions syncing code falsely thinking that no external account
exists for this authz provider.

So why even use the VISUAL_STUDIO_APP_URL as the service ID in the auth
provider? This is because that's the URL that is used for all oauth2
requests. While the API for product features like repos, orgs, user
profiles is pointed to by the value of AZURE_DEV_OPS_API_URL leading to
a deviation in expectations.
Instead of continuing to use the `authorization` pattern similar to
other code hosts, it is simpler to add a boolean which has an explicit
name and describes what it does better.

The end result: we start to break away from the `authorization`
pattern (some other code hosts can do that as well, maybe this is the
start of it all) with an additional check for this new field in the
write path of external_services.Create, where we check to ensure the
correct value of `unrestricted` is written to the database.

For existing auth providers, this is forwards compatible. While at the
same time if they also wanted to get on the `enforcePermissions` way of
life in the future, the change to `recalculateFields` will let them be
backwards compatible.

Also a small tweak to the doc string to make things less confusing.
@indradhanush indradhanush force-pushed the ig/ado-user-perms-backup branch 2 times, most recently from 90610ff to 7ff128d Compare February 28, 2023 12:06
@indradhanush indradhanush changed the title Ig/ado user perms backup ADO: Implement user permissions sync Feb 28, 2023
@indradhanush indradhanush marked this pull request as ready for review February 28, 2023 12:10
@indradhanush indradhanush force-pushed the ig/ado-user-perms-backup branch from 7ff128d to b0773d0 Compare February 28, 2023 12:10
@indradhanush indradhanush requested review from a team February 28, 2023 12:17
@indradhanush indradhanush self-assigned this Feb 28, 2023
@indradhanush indradhanush added feature/permissions ADO Tier 1 support for Azure DevOps labels Feb 28, 2023
@indradhanush

Copy link
Copy Markdown
Contributor Author

After chatting with @kopancek and @pjlast on Slack, I've added a fix for handling multiple code host connections. I will add a test for this tomorrow as I need to step out now. Ran out of time. ⌛

@sashaostrikov sashaostrikov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great work! Left a bunch of comments, but nothing is critical, mostly related to how the code is written and not about the correctness of implementation :)

Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment on lines +116 to +122

l.Debug("skipping org",
log.String("org", org),
log.Int("http status code", httpErr.StatusCode),
)

continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd make this log a WARN (and change the message a bit), because this errors if not surfaced, should be properly logged at least.
Also, removing unnecessary empty lines

Suggested change
l.Debug("skipping org",
log.String("org", org),
log.Int("http status code", httpErr.StatusCode),
)
continue
l.Warn("failed to list organization repositories",
log.String("org", org),
log.Int("http status code", httpErr.StatusCode),
)
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same comment applies to below projects piece of logic

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm. I wouldn't log it as a warn since that will show up on more or less every permissions sync - I'm guessing not all users have access to all orgs / projects. I'm keeping it as a DEBUG for times if we have to debug a permissions issue and we want to verify that this org or project is explicitly being skipped because of a 4xx error - that is the user does not have access to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

okay, in this case I would make the log message a bit more elaborate. seeing "skipping org" in logs would make me confused :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea. I changed it to: user does not have access to this org. What do you think?

Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread internal/extsvc/azuredevops/client.go
Comment thread internal/extsvc/azuredevops/types.go Outdated
Comment thread internal/extsvc/azuredevops/users.go Outdated
Comment thread internal/oauthutil/oauth2.go Outdated
Comment thread internal/oauthutil/token.go Outdated
Comment thread enterprise/cmd/frontend/internal/auth/azureoauth/provider.go Outdated
Comment thread enterprise/cmd/frontend/internal/auth/azureoauth/session.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread internal/extsvc/azuredevops/client.go Outdated
Comment thread internal/oauthutil/oauth2.go Outdated
Comment thread internal/oauthutil/token.go Outdated
Comment on lines +22 to +26
"enforcePermissions": {
"description": "A flag to enforce Azure DevOps repository access permissions",
"type": "boolean",
"default": false
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please, add docs for this once you start making docs changes. This is a very big step forward, but also completely different to what we configure for other code hosts to enable permissions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes absolutely. I am going to add a doc on configuring Azure OAuth provider and will cover this in there. Is that what you were also suggesting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So this replaces the authorization: {} thing from the other code host connections, yes?

It kinda makes me nervous that we do this only for one new provider now. Somehow I'd rather have us introduce to every provider at the same time and keep backwards compatibility. But I guess we can still do that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree why it could make anyone nervous. When I was looking at this - the change turned out to be reasonably simple and easy to implement: https://github.com/sourcegraph/sourcegraph/pull/48209/files#diff-e64c2422ddc2f4da136695fcb608eee988e39ac2fb9ed1975646bf3f9bba7766R1612-R1615

Which gave me a high degree of confidence that this will not break for existing code hosts on the authorization: {} pattern. And it felt weird to continue to add on to this pattern as a result since the end user site-admin experience of configuring Azure DevOps with permisisons sync wouldn't be very intuitive.

Release timing wise this might be a better time to introduce this pattern limited to Azure DevOps and then refactor the existing ones on the new pattern after the upcoming release so we have sufficient time with testing and knowing if anything has gone wrong until the next release.

Comment on lines +197 to +202
// DEBUGGING NOTE: The token refresher (internal/oauthutil/token.go:newTokenRequest)
// adds some default key-value pairs to the body, some of which are eventually
// overridden by the values in this map. But some extra arg remain in the body that
// is sent in the request. This works for now, but if refreshing a token ever stops
// working for Azure Dev Ops this is a good place to start looking by writing a
// custom implementation that only sends the key-value pairs that the API expects.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since you are aware of this problem, wouldn't it be a better architecture to actually do what you suggest? E.g. add a function on the context that would provide URL query parameters and default to the original one if it's not present?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If by context you mean OAuthContext here, then from what I understand you're suggesting:

  1. Add a method on OAuthContext like: func (o *OAuthContext) AddExtraArgs()
  2. Call that instead of reading from CustomQueryParams?

If it's not a blocker, I would like to keep it as is - for now, since this PR is big enough and because we do a very similar workflow in the OAuth login by passing in extra args as part of the Exchange method call.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, that's what I had in mind. Not a blocker, just a suggestion for future

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Noted. I can follow up once this is merged. 👍

Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread internal/extsvc/azuredevops/client.go Outdated
@indradhanush indradhanush force-pushed the ig/ado-user-perms-backup branch from 4d7abe7 to 5616b9c Compare March 1, 2023 13:28
@sourcegraph-bot

sourcegraph-bot commented Mar 1, 2023

Copy link
Copy Markdown
Contributor

Codenotify: Notifying subscribers in CODENOTIFY files for diff bb06945...c19633e.

Notify File(s)
@eseliger internal/database/external_services.go
internal/extsvc/azuredevops/client.go
internal/extsvc/azuredevops/types.go
internal/extsvc/azuredevops/users.go
@unknwon enterprise/cmd/frontend/internal/auth/azureoauth/login.go
enterprise/cmd/frontend/internal/auth/azureoauth/provider.go
enterprise/cmd/frontend/internal/auth/azureoauth/session.go
enterprise/internal/authz/authz.go
enterprise/internal/authz/authz_test.go
enterprise/internal/authz/azuredevops/provider.go
enterprise/internal/authz/azuredevops/provider_test.go

@indradhanush indradhanush requested review from a team March 1, 2023 14:41
Comment thread enterprise/internal/authz/azuredevops/provider.go

@sashaostrikov sashaostrikov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

At least all of my comments are resolved -- LGTM :shipit:

@kopancek kopancek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving to unblock, but left some comments for future PR maybe :)

Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go
}

func (p *Provider) FetchRepoPerms(ctx context.Context, repo *extsvc.Repository, opts authz.FetchPermsOptions) ([]extsvc.AccountID, error) {
return nil, authz.ErrUnimplemented{Feature: "azuredevops.FetchRepoPerms"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you add a comment about it to the code?

}

func GetOAuthContext(refreshToken string) (*oauthutil.OAuthContext, error) {
for _, authProvider := range conf.SiteConfig().AuthProviders {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I still think the above ☝️ would be a nice improvement to the code.

Comment thread internal/extsvc/azuredevops/client.go
Comment on lines +197 to +202
// DEBUGGING NOTE: The token refresher (internal/oauthutil/token.go:newTokenRequest)
// adds some default key-value pairs to the body, some of which are eventually
// overridden by the values in this map. But some extra arg remain in the body that
// is sent in the request. This works for now, but if refreshing a token ever stops
// working for Azure Dev Ops this is a good place to start looking by writing a
// custom implementation that only sends the key-value pairs that the API expects.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, that's what I had in mind. Not a blocker, just a suggestion for future

Comment thread enterprise/internal/authz/azuredevops/provider.go Outdated
Comment thread enterprise/internal/authz/azuredevops/provider.go
Comment thread enterprise/internal/authz/azuredevops/provider.go
"description": "A flag to enforce Azure DevOps repository access permissions",
"type": "boolean",
"default": false
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

GitHub won't allow me to comment further down, so I'm going to leave the comment here.
We can consider making the "what repos do I sync" part of the config a bit more expressive. Here's the scenario as I see it:

  • There are organizations, projects, and repositories
  • If you want to sync all repositories from an organization, you only specify that organization. It is redundant to be able to specify org/project if you've already specified org
  • Ditto for org/project/repository
  • So, construct the object in such a way that we get an object as follows:
"repositories": [
  {
    "org": "orgName", // required
    "projects": [ // optional
      {
        "project": "projectName", // required
        "repositoryIds": [ "1", "2" ] // optional
      }
    ]
  }
]

So now you cannot specify redundant fields. You either specify an entire org, or you specify an org and some projects, or you specify an org and some projects and repo IDs for some projects. Or any combination of these.

Not a blocker, but I think "ergonomically" it makes a lot of sense.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a good callout but I'm also not sure if the propsed structure would be less confusing since we nest org and projects under repositories - which feels wrong hierarchically since repos are part of an org and not the other way round.

We could however make our backend smarter to ignore projects if orgs have already been synced.

Let's make this a separate discussion and get others opinions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I mean I just called it repositories to give it a name. It's just a JSON key you can name it anything

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

But yes, splitting the projects by org and dropping redundant ones is the other option

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines +35 to +48
for _, c := range conns {
// The list of orgs and projects may have duplicates if there are multiple Azure DevOps code
// host connections that have the same project in their config.
//
// Add them to a map so that we may filter out duplicates before passing them over to the
// provider.
for _, name := range c.Orgs {
orgs[name] = struct{}{}
}

for _, name := range c.Projects {
projects[name] = struct{}{}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One thing I forgot to mention here: you need to make sure that you are only doing this for connections with authorization enabled, otherwise perms syncing will possibly be fetching a whole lot of redundant orgs and projects.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just saw this. Will fix in a follow up.

@indradhanush indradhanush Mar 2, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow up here: #48549

@indradhanush indradhanush merged commit 4b62fa2 into main Mar 2, 2023
@indradhanush indradhanush deleted the ig/ado-user-perms-backup branch March 2, 2023 12:15
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ADO Tier 1 support for Azure DevOps cla-signed feature/permissions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants