Skip to content

Commit

Permalink
Merge pull request #5897 from concourse/issue/5851
Browse files Browse the repository at this point in the history
Generate opaque access tokens
  • Loading branch information
aoldershaw authored Aug 4, 2020
2 parents 51a6c51 + c2fc265 commit 6a58961
Show file tree
Hide file tree
Showing 62 changed files with 2,153 additions and 1,339 deletions.
141 changes: 56 additions & 85 deletions atc/api/accessor/accessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type access struct {
systemClaimKey string
systemClaimValues []string
teams []db.Team
teamRoles map[string][]string
isAdmin bool
}

func NewAccessor(
Expand All @@ -51,77 +53,42 @@ func NewAccessor(
systemClaimValues []string,
teams []db.Team,
) *access {
return &access{
a := &access{
verification: verification,
requiredRole: requiredRole,
systemClaimKey: systemClaimKey,
systemClaimValues: systemClaimValues,
teams: teams,
}
a.computeTeamRoles()
return a
}

func (a *access) HasToken() bool {
return a.verification.HasToken
}

func (a *access) IsAuthenticated() bool {
return a.verification.IsTokenValid
}

func (a *access) IsAuthorized(teamName string) bool {

if a.IsAdmin() {
return true
}

for _, team := range a.TeamNames() {
if team == teamName {
return true
}
}

return false
}

func (a *access) TeamNames() []string {

teamNames := []string{}

isAdmin := a.IsAdmin()
func (a *access) computeTeamRoles() {
a.teamRoles = map[string][]string{}

for _, team := range a.teams {
if isAdmin || a.hasRequiredRole(team.Auth()) {
teamNames = append(teamNames, team.Name())
roles := a.rolesForTeam(team.Auth())
if len(roles) > 0 {
a.teamRoles[team.Name()] = roles
}
if team.Admin() && contains(roles, "owner") {
a.isAdmin = true
}
}

return teamNames
}

func (a *access) hasRequiredRole(auth atc.TeamAuth) bool {
for _, teamRole := range a.rolesForTeam(auth) {
if a.hasPermission(teamRole) {
func contains(arr []string, val string) bool {
for _, v := range arr {
if v == val {
return true
}
}
return false
}

func (a *access) teamRoles() map[string][]string {

teamRoles := map[string][]string{}

for _, team := range a.teams {
if roles := a.rolesForTeam(team.Auth()); len(roles) > 0 {
teamRoles[team.Name()] = roles
}
}

return teamRoles
}

func (a *access) rolesForTeam(auth atc.TeamAuth) []string {

roleSet := map[string]bool{}

groups := a.groups()
Expand Down Expand Up @@ -169,19 +136,45 @@ func (a *access) rolesForTeam(auth atc.TeamAuth) []string {
return roles
}

func (a *access) hasPermission(role string) bool {
switch a.requiredRole {
case OwnerRole:
return role == OwnerRole
case MemberRole:
return role == OwnerRole || role == MemberRole
case OperatorRole:
return role == OwnerRole || role == MemberRole || role == OperatorRole
case ViewerRole:
return role == OwnerRole || role == MemberRole || role == OperatorRole || role == ViewerRole
default:
return false
func (a *access) HasToken() bool {
return a.verification.HasToken
}

func (a *access) IsAuthenticated() bool {
return a.verification.IsTokenValid
}

func (a *access) IsAuthorized(teamName string) bool {
return a.isAdmin || a.hasPermission(a.teamRoles[teamName])
}

func (a *access) TeamNames() []string {
teamNames := []string{}
for _, team := range a.teams {
if a.isAdmin || a.hasPermission(a.teamRoles[team.Name()]) {
teamNames = append(teamNames, team.Name())
}
}

return teamNames
}

func (a *access) hasPermission(roles []string) bool {
for _, role := range roles {
switch a.requiredRole {
case OwnerRole:
return role == OwnerRole
case MemberRole:
return role == OwnerRole || role == MemberRole
case OperatorRole:
return role == OwnerRole || role == MemberRole || role == OperatorRole
case ViewerRole:
return role == OwnerRole || role == MemberRole || role == OperatorRole || role == ViewerRole
default:
return false
}
}
return false
}

func (a *access) claims() map[string]interface{} {
Expand Down Expand Up @@ -244,30 +237,8 @@ func (a *access) groups() []string {
return groups
}

func (a *access) adminTeams() []string {
var adminTeams []string

for _, team := range a.teams {
if team.Admin() {
adminTeams = append(adminTeams, team.Name())
}
}
return adminTeams
}

func (a *access) IsAdmin() bool {

teamRoles := a.teamRoles()

for _, adminTeam := range a.adminTeams() {
for _, role := range teamRoles[adminTeam] {
if role == "owner" {
return true
}
}
}

return false
return a.isAdmin
}

func (a *access) IsSystem() bool {
Expand All @@ -282,7 +253,7 @@ func (a *access) IsSystem() bool {
}

func (a *access) TeamRoles() map[string][]string {
return a.teamRoles()
return a.teamRoles
}

func (a *access) Claims() Claims {
Expand Down
43 changes: 41 additions & 2 deletions atc/api/accessor/accessor_factory.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,63 @@
package accessor

import (
"fmt"
"net/http"

"github.com/concourse/concourse/atc/db"
)

//go:generate counterfeiter . TokenVerifier

type TokenVerifier interface {
Verify(req *http.Request) (map[string]interface{}, error)
}

//go:generate counterfeiter . TeamFetcher

type TeamFetcher interface {
GetTeams() ([]db.Team, error)
}

func NewAccessFactory(
tokenVerifier TokenVerifier,
teamFetcher TeamFetcher,
systemClaimKey string,
systemClaimValues []string,
) AccessFactory {
return &accessFactory{
tokenVerifier: tokenVerifier,
teamFetcher: teamFetcher,
systemClaimKey: systemClaimKey,
systemClaimValues: systemClaimValues,
}
}

type accessFactory struct {
tokenVerifier TokenVerifier
teamFetcher TeamFetcher
systemClaimKey string
systemClaimValues []string
}

func (a *accessFactory) Create(role string, verification Verification, teams []db.Team) Access {
return NewAccessor(verification, role, a.systemClaimKey, a.systemClaimValues, teams)
func (a *accessFactory) Create(req *http.Request, role string) (Access, error) {
teams, err := a.teamFetcher.GetTeams()
if err != nil {
return nil, fmt.Errorf("fetch teams: %w", err)
}
return NewAccessor(a.verifyToken(req), role, a.systemClaimKey, a.systemClaimValues, teams), nil
}

func (a *accessFactory) verifyToken(req *http.Request) Verification {
claims, err := a.tokenVerifier.Verify(req)
if err != nil {
switch err {
case ErrVerificationNoToken:
return Verification{HasToken: false, IsTokenValid: false}
default:
return Verification{HasToken: true, IsTokenValid: false}
}
}

return Verification{HasToken: true, IsTokenValid: true, RawClaims: claims}
}
93 changes: 82 additions & 11 deletions atc/api/accessor/accessor_factory_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package accessor_test

import (
"errors"
"net/http"

"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/api/accessor/accessorfakes"
"github.com/concourse/concourse/atc/db/dbfakes"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

Expand All @@ -13,31 +19,96 @@ var _ = Describe("AccessorFactory", func() {
systemClaimKey string
systemClaimValues []string

role string
verification accessor.Verification
teams []db.Team
fakeTokenVerifier *accessorfakes.FakeTokenVerifier
fakeTeamFetcher *accessorfakes.FakeTeamFetcher
dummyRequest *http.Request

role string
)

BeforeEach(func() {
systemClaimKey = "sub"
systemClaimValues = []string{"some-sub"}

role = "some-role"
verification = accessor.Verification{}
teams = []db.Team{}
fakeTokenVerifier = new(accessorfakes.FakeTokenVerifier)
fakeTeamFetcher = new(accessorfakes.FakeTeamFetcher)
dummyRequest, _ = http.NewRequest("GET", "/", nil)

role = "viewer"
})

Describe("Create", func() {

var access accessor.Access
var (
access accessor.Access
err error
)

JustBeforeEach(func() {
factory := accessor.NewAccessFactory(systemClaimKey, systemClaimValues)
access = factory.Create(role, verification, teams)
factory := accessor.NewAccessFactory(fakeTokenVerifier, fakeTeamFetcher, systemClaimKey, systemClaimValues)
access, err = factory.Create(dummyRequest, role)
})

It("creates an accessor", func() {
Expect(access).NotTo(BeNil())
Context("when the token is valid", func() {
BeforeEach(func() {
fakeTokenVerifier.VerifyReturns(map[string]interface{}{
"federated_claims": map[string]interface{}{
"connector_id": "github",
"user_name": "user1",
},
}, nil)
teamWithUsers := func(name string, authenticated bool) db.Team {
t := new(dbfakes.FakeTeam)
t.NameReturns(name)
if authenticated {
t.AuthReturns(atc.TeamAuth{"viewer": map[string][]string{
"users": {"github:user1"},
}})
}
return t
}
fakeTeamFetcher.GetTeamsReturns([]db.Team{
teamWithUsers("t1", true),
teamWithUsers("t2", false),
teamWithUsers("t3", true),
}, nil)
})

It("returns an accessor with the correct teams", func() {
Expect(access.TeamNames()).To(ConsistOf("t1", "t3"))
})
})

Context("when the team fetcher returns an error", func() {
BeforeEach(func() {
fakeTeamFetcher.GetTeamsReturns(nil, errors.New("nope"))
})

It("returns an error", func() {
Expect(err).To(HaveOccurred())
})
})

Context("when the verifier returns a NoToken error", func() {
BeforeEach(func() {
fakeTokenVerifier.VerifyReturns(nil, accessor.ErrVerificationNoToken)
})

It("the accessor has no token", func() {
Expect(err).ToNot(HaveOccurred())
Expect(access.HasToken()).To(BeFalse())
})
})

Context("when the verifier returns some other error", func() {
BeforeEach(func() {
fakeTokenVerifier.VerifyReturns(nil, accessor.ErrVerificationTokenExpired)
})

It("the accessor is unauthenticated", func() {
Expect(err).ToNot(HaveOccurred())
Expect(access.IsAuthenticated()).To(BeFalse())
})
})
})
})
1 change: 1 addition & 0 deletions atc/api/accessor/accessor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ var _ = Describe("Accessor", func() {

verification.HasToken = true
verification.IsTokenValid = true

verification.RawClaims = map[string]interface{}{
"groups": []interface{}{"some-group"},
"federated_claims": map[string]interface{}{
Expand Down
Loading

0 comments on commit 6a58961

Please sign in to comment.