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

connector/github: multiple orgs, query by teams #1013

Merged
merged 1 commit into from
Aug 8, 2017

Conversation

estroz
Copy link
Contributor

@estroz estroz commented Aug 3, 2017

connector/github: support for multiple orgs and teams within orgs; allow filtering by both orgs and teams

Documentation: examples of GitHub orgs field with multiple orgs and org with teams

The GitHub connector should give more fine-grained control over which organizations and teams have access to an application. Restricting authorization to (multiple) teams within (multiple) orgs by specifying them in the connector config section gives dex users such control.

Tests: set up organizations with/out teams and a test user; went through scenarios listed in this function description

Note: this PR obsoletes #1008 because similar errors are returned if user is not a org member

@@ -34,16 +34,22 @@ type Config struct {
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
RedirectURI string `json:"redirectURI"`
Org string `json:"org"`
Copy link
Contributor

Choose a reason for hiding this comment

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

We should keep this field and behavior around to not break existing users.

Copy link
Contributor

Choose a reason for hiding this comment

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

You've still removed the feature.

Copy link
Contributor

Choose a reason for hiding this comment

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

@estroz btw this behavior has changed. You'll start prefixing "(org):" to any group.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding legacy logic now.

HostName string `json:"hostName"`
RootCA string `json:"rootCA"`
}

// Org holds org-team filters, in which teams are optional
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to explain the impact of providing the teams field. Also the logic about what teams or orgs a user has to belong to to login.

// N orgs, no teams: user is member of at least 1 org
// N orgs, M teams per org: user is member of any team from at least 1 org
// N-1 orgs, M teams per org, 1 org with no teams: user is member of any team from at least 1 org, or member of org with no teams
func (c *githubConnector) isUserAuthorized(ctx context.Context, client *http.Client, userName string) (groups []string, isAuthorized bool, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Method name could probably be more specific. Maybe just listGroups

Also two orgs can have teams with the same name. We probably need to come up with some naming schema for the groups. e.g. ( org ):( team ) "coreos:engineers"

err = fmt.Errorf("unexpected return status (code %d): %q", resp.StatusCode, resp.Status)
}

// 204 if user is a member
Copy link
Contributor

Choose a reason for hiding this comment

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

Ha. Fun API.


err = nil
switch resp.StatusCode {
case http.StatusNoContent, http.StatusFound, http.StatusNotFound:
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably want to map these to a message about what they represent https://developer.github.com/v3/orgs/members/#check-membership

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Status(Not)Found mean the same thing in our context (requester == user), so I'll return a 'user not in org' error

}
defer resp.Body.Close()

err = nil
Copy link
Contributor

Choose a reason for hiding this comment

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

err is already nil

isAuthorized = true
groups = append(groups, teams...)
c.logger.Debugf("github: user %q in org %q", userName, org.Name)
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

Really confused by this continue and the one following it. Not clear what you're trying to do here.

return userTeams, true, nil
}

wantedTeams := make(map[string]struct{})
Copy link
Contributor

Choose a reason for hiding this comment

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

wantedTeams is probably a bad name here. Can probably just write this as a method

func filterTeams(whiteList, userTeams []string) []string

// The HTTP passed client is expected to be constructed by the golang.org/x/oauth2 package,
// which inserts a bearer token as part of the request.
func (c *githubConnector) userInOrgTeams(ctx context.Context, client *http.Client, userName string, org Org) ([]string, bool, error) {
if inOrg, err := c.userInOrg(ctx, client, userName, org.Name); err != nil || !inOrg {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this logic just be done in isUserAuthorized?

@estroz estroz force-pushed the multi-org-team-filters branch 5 times, most recently from e1e8a52 to c950d82 Compare August 7, 2017 19:34
}

if teams, err = c.teams(ctx, client, org.Name); err != nil {
c.logger.Errorf("teams: %s", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just return this error immediately.

for _, org := range c.orgs {
inOrg, err = c.userInOrg(ctx, client, userName, org.Name)
if err != nil {
c.logger.Errorf("user in org: %s", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just return this error immediately.

// User is in at least in one org. No team restrictions are imposed from
// config. We add all the teams in the organization the user is a member of
// to 'groups' to preserve legacy behavior.
if len(org.Teams) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

if len(org.Teams) > 0 {
    teams = filterTeams(teams, org.Teams)
}

groups = append(groups, joinOrgToTeams(org.Name, teams)...)

No need to have these two continue statements.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How about something like:

orgPrefix := org.Name + ":"
if len(org.Teams) > 0 {
  for _, teamName := range filterTeams(teams, org.Teams) {
    groups = append(groups, orgPrefix+teamName)
  }
} else {
  groups = append(groups, orgPrefix)
}

If teams are specified in the config file, then check if the user is a team member and add those teams to groups. Otherwise, use an org-level group "orgName:" to indicate that the user is an org member.


// Orgs might have the same team names. We append the orgName to team name
// delimited by a colon, "org:team", to make team names unique across orgs.
func joinOrgToTeams(orgName string, teamNames []string) []string {
Copy link
Contributor

Choose a reason for hiding this comment

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

doesn't need to be it's own method.

groups = append(groups, orgPrefix+teamName)
c.logger.Debugf("github: user %q in org %q team %q", userName, org.Name, teamName)
}
} else { // No team restrictions. Append "org:" to groups
Copy link
Contributor

@ericchiang ericchiang Aug 7, 2017

Choose a reason for hiding this comment

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

I really don't like this group value. I'd rather people just evaluate teams, not orgs for auth-Z decisions.

@@ -34,16 +34,22 @@ type Config struct {
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
RedirectURI string `json:"redirectURI"`
Org string `json:"org"`
Copy link
Contributor

Choose a reason for hiding this comment

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

@estroz btw this behavior has changed. You'll start prefixing "(org):" to any group.

@estroz estroz force-pushed the multi-org-team-filters branch 3 times, most recently from 054ed7b to 2d48bc8 Compare August 7, 2017 22:21
teams:
- read-team
- blue-team
- name: my-organization-no-teams
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to document why these two are different.

# Optional organization to pull teams from, communicate through the
# "groups" scope.
# Optional organizations and teams, communicated through the
# "groups" scope. The 'org' field is kept for legacy users.
Copy link
Contributor

Choose a reason for hiding this comment

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

Lots of "legacy" references. I'm starting to feel old :)

We can probably just drop these docs.

@estroz estroz force-pushed the multi-org-team-filters branch from 2d48bc8 to 670e23e Compare August 7, 2017 23:18
# this field to restrict access to the specified GitHub org.
# org: my-organization
#
# New 'orgs' field. 'orgs' supports organizations with or without teams.
Copy link
Contributor

Choose a reason for hiding this comment

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

Few nits.

  • This field is 'new' in the release notes, for docs it just is.
  • "restrict access" is probably the wrong phrase.
  • Let's just remove the NOTE: ... line and Legacy 'org' field... block.

Maybe something like this:

# Query the following organizations for group information if the "groups"
# scope is provided. Group claims are formatted as "(org):(team)". For
# example if a user is part of the "engineering" team of the "coreos" org,
# the group claim would include "coreos:engineering"
#
# A user MUST be a member of at least one of the following orgs to
# authenticate with dex.
orgs:
- name: my-org
  # Include all teams as claims.
- name: my-org-with-teams
  # A white list of teams. Only include group claims for these teams.
  teams:
  - read-team
  - blue-team

@estroz estroz force-pushed the multi-org-team-filters branch 2 times, most recently from 46ed036 to 589aa35 Compare August 7, 2017 23:52
@ericchiang
Copy link
Contributor

@estroz lgtm. Have you tested this?

@estroz
Copy link
Contributor Author

estroz commented Aug 8, 2017

@ericchiang yes, although I'm going through the process once more to be sure.

// from at least 1 org, or member of org with no teams
func (c *githubConnector) listGroups(ctx context.Context, client *http.Client, userName string) ([]string, bool, error) {
var (
inOrg, inOrgNoTeams bool
Copy link
Contributor

Choose a reason for hiding this comment

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

inOrgNoTeams is never set. It's always false.

func (c *githubConnector) listGroups(ctx context.Context, client *http.Client, userName string) ([]string, bool, error) {
var (
inOrg, inOrgNoTeams bool
teams, groups []string
Copy link
Contributor

Choose a reason for hiding this comment

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

inOrg, teams, and err do not need to be defined outside the for loop. Just define them as used in the for loop,

@estroz estroz force-pushed the multi-org-team-filters branch from 589aa35 to cba6ba0 Compare August 8, 2017 01:31
@estroz
Copy link
Contributor Author

estroz commented Aug 8, 2017

@ericchiang tested with both orgs and org fields and made some updates. works as expected in all scenarios described by docs.

// N orgs, M teams per org: user is member of any team from at least 1 org
// N-1 orgs, M teams per org, 1 org with no teams: user is member of any team
// from at least 1 org, or member of org with no teams
func (c *githubConnector) listGroups(ctx context.Context, client *http.Client, userName string) (groups []string, err error) {
Copy link
Contributor

@ericchiang ericchiang Aug 8, 2017

Choose a reason for hiding this comment

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

If a user isn't in any of the orgs, this will still return []string{}, nil and the user will login successfully.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Fixing now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed and tested.

Documentation: examples of GitHub `orgs` field with multiple orgs
and org with teams; note legacy behavior
@estroz estroz force-pushed the multi-org-team-filters branch from cba6ba0 to 9d15480 Compare August 8, 2017 17:58
Copy link
Contributor

@ericchiang ericchiang left a comment

Choose a reason for hiding this comment

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

lgtm

@estroz estroz merged commit 45bf061 into dexidp:master Aug 8, 2017
@estroz estroz deleted the multi-org-team-filters branch August 8, 2017 18:37
mmrath pushed a commit to mmrath/dex that referenced this pull request Sep 2, 2019
connector/github: multiple orgs, query by teams
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.

2 participants