Skip to content

Commit

Permalink
Implement TFE API for Team Tokens (#624)
Browse files Browse the repository at this point in the history
Towards #580 

This is the initial API implementation for Team Tokens. The TFE
integration tests pass, but I don't think the token can actually be used
to do anything as yet.

I've added `Team` as a sort of half-concept for the RBAC. It's not a
full category in it's own right, but uses the `User` team memberships to
authorize. Not sure if that's desirable, but it seemed a minimal effort
fit as I understand how the RBAC works.

---------

Co-authored-by: Louis Garman <louisgarman@gmail.com>
  • Loading branch information
tomwardill-payoneer and leg100 authored Oct 26, 2023
1 parent 76e7dda commit 1e4b173
Show file tree
Hide file tree
Showing 28 changed files with 836 additions and 130 deletions.
1 change: 1 addition & 0 deletions hack/go-tfe-tests.bash
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ tests+=('TestTeamsCreate')
tests+=('TestTeamsRead')
tests+=('TestTeamsUpdate$')
tests+=('TestTeamsDelete')
tests+=('TestTeamToken')
tests+=('TestConfigurationVersionsList')
tests+=('TestConfigurationVersionsCreate')
tests+=('TestConfigurationVersionsUpload')
Expand Down
65 changes: 65 additions & 0 deletions internal/auth/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"time"

"github.com/leg100/otf/internal"
"github.com/leg100/otf/internal/rbac"
)

type (
Expand Down Expand Up @@ -115,10 +116,74 @@ func newTeam(organization string, opts CreateTeamOptions) (*Team, error) {
func (t *Team) String() string { return t.Name }
func (t *Team) OrganizationAccess() OrganizationAccess { return t.Access }

func (t *Team) IsSiteAdmin() bool { return false }

func (t *Team) IsOwners() bool {
return t.Name == "owners"
}

func (t *Team) IsOwner(organization string) bool {
return t.Organization == organization && t.IsOwners()
}

func (t *Team) CanAccessSite(action rbac.Action) bool {
return false
}

func (t *Team) CanAccessTeam(action rbac.Action, id string) bool {
// team can access self
return t.ID == id
}

func (t *Team) CanAccessOrganization(action rbac.Action, org string) bool {
if t.Organization == org {
if t.IsOwners() {
// owner team can perform all actions on organization
return true
}
if rbac.OrganizationMinPermissions.IsAllowed(action) {
return true
}
if t.Access.ManageWorkspaces {
if rbac.WorkspaceManagerRole.IsAllowed(action) {
return true
}
}
if t.Access.ManageVCS {
if rbac.VCSManagerRole.IsAllowed(action) {
return true
}
}
if t.Access.ManageModules {
if rbac.VCSManagerRole.IsAllowed(action) {
return true
}
}
}
return false
}

func (t *Team) CanAccessWorkspace(action rbac.Action, policy internal.WorkspacePolicy) bool {
// coarser-grained organization perms take precedence.
if t.CanAccessOrganization(action, policy.Organization) {
return true
}
// fallback to checking finer-grained workspace perms
if t.Organization != policy.Organization {
return false
}
for _, perm := range policy.Permissions {
if t.Name == perm.Team {
return perm.Role.IsAllowed(action)
}
}
return false
}

func (t *Team) Organizations() []string {
return []string{t.Organization}
}

func (t *Team) Update(opts UpdateTeamOptions) error {
if opts.Name != nil {
t.Name = *opts.Name
Expand Down
26 changes: 26 additions & 0 deletions internal/auth/team_authorizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package auth

import (
"context"

"github.com/go-logr/logr"
"github.com/leg100/otf/internal"
"github.com/leg100/otf/internal/rbac"
)

// Authorizer authorizes access to a team
type Authorizer struct {
logr.Logger
}

func (a *Authorizer) CanAccess(ctx context.Context, action rbac.Action, teamID string) (internal.Subject, error) {
subj, err := internal.SubjectFromContext(ctx)
if err != nil {
return nil, err
}
if subj.CanAccessTeam(action, teamID) {
return subj, nil
}
a.Error(nil, "unauthorized action", "team_id", teamID, "action", action.String(), "subject", subj)
return nil, internal.ErrAccessNotPermitted
}
8 changes: 8 additions & 0 deletions internal/auth/team_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ func (db *pgdb) getTeamByID(ctx context.Context, id string) (*Team, error) {
return teamRow(result).toTeam(), nil
}

func (db *pgdb) getTeamByTokenID(ctx context.Context, tokenID string) (*Team, error) {
result, err := db.Conn(ctx).FindTeamByTokenID(ctx, sql.String(tokenID))
if err != nil {
return nil, sql.Error(err)
}
return teamRow(result).toTeam(), nil
}

func (db *pgdb) getTeamForUpdate(ctx context.Context, id string) (*Team, error) {
result, err := db.Conn(ctx).FindTeamByIDForUpdate(ctx, sql.String(id))
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions internal/auth/team_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type TeamService interface {
CreateTeam(ctx context.Context, organization string, opts CreateTeamOptions) (*Team, error)
GetTeam(ctx context.Context, organization, team string) (*Team, error)
GetTeamByID(ctx context.Context, teamID string) (*Team, error)
GetTeamByTokenID(ctx context.Context, teamTokenID string) (*Team, error)
ListTeams(ctx context.Context, organization string) ([]*Team, error)
ListTeamMembers(ctx context.Context, teamID string) ([]*User, error)
UpdateTeam(ctx context.Context, teamID string, opts UpdateTeamOptions) (*Team, error)
Expand Down Expand Up @@ -200,3 +201,20 @@ func (a *service) createOwnersTeam(ctx context.Context, organization *organizati
return nil
})
}

func (a *service) GetTeamByTokenID(ctx context.Context, tokenID string) (*Team, error) {
team, err := a.db.getTeamByTokenID(ctx, tokenID)
if err != nil {
a.Error(err, "retrieving team by team token ID", "token_id", tokenID)
return nil, err
}

subject, err := a.organization.CanAccess(ctx, rbac.GetTeamAction, team.Organization)
if err != nil {
return nil, err
}

a.V(9).Info("retrieved team", "team", team.Name, "organization", team.Organization, "subject", subject)

return team, nil
}
53 changes: 19 additions & 34 deletions internal/auth/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,36 +136,28 @@ func (u *User) CanAccessSite(action rbac.Action) bool {
return u.IsSiteAdmin()
}

func (u *User) CanAccessTeam(action rbac.Action, teamID string) bool {
// coarser-grained site-level perms take precedence
if u.CanAccessSite(action) {
return true
}
for _, team := range u.Teams {
if team.ID == teamID {
return true
}
}
return false
}

func (u *User) CanAccessOrganization(action rbac.Action, org string) bool {
// coarser-grained site-level perms take precedence
if u.CanAccessSite(action) {
return true
}
// fallback to finer-grained organization-level perms
for _, team := range u.Teams {
if team.Organization == org {
if team.IsOwners() {
// owner team members can perform all actions on organization
return true
}
if rbac.OrganizationMinPermissions.IsAllowed(action) {
return true
}
if team.Access.ManageWorkspaces {
if rbac.WorkspaceManagerRole.IsAllowed(action) {
return true
}
}
if team.Access.ManageVCS {
if rbac.VCSManagerRole.IsAllowed(action) {
return true
}
}
if team.Access.ManageModules {
if rbac.VCSManagerRole.IsAllowed(action) {
return true
}
}
if team.CanAccessOrganization(action, org) {
return true
}
}
return false
Expand All @@ -178,13 +170,8 @@ func (u *User) CanAccessWorkspace(action rbac.Action, policy internal.WorkspaceP
}
// fallback to checking finer-grained workspace perms
for _, team := range u.Teams {
if team.Organization != policy.Organization {
continue
}
for _, perm := range policy.Permissions {
if team.Name == perm.Team {
return perm.Role.IsAllowed(action)
}
if team.CanAccessWorkspace(action, policy) {
return true
}
}
return false
Expand All @@ -193,10 +180,8 @@ func (u *User) CanAccessWorkspace(action rbac.Action, policy internal.WorkspaceP
// IsOwner determines if user is an owner of an organization
func (u *User) IsOwner(organization string) bool {
for _, team := range u.Teams {
if team.Organization == organization {
if team.IsOwners() {
return true
}
if team.IsOwner(organization) {
return true
}
}
return false
Expand Down
2 changes: 2 additions & 0 deletions internal/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const subjectCtxKey subjectCtxKeyType = "subject"
// Subject is an entity that carries out actions on resources.
type Subject interface {
CanAccessSite(action rbac.Action) bool
CanAccessTeam(action rbac.Action, id string) bool
CanAccessOrganization(action rbac.Action, name string) bool
CanAccessWorkspace(action rbac.Action, policy WorkspacePolicy) bool

Expand Down Expand Up @@ -65,6 +66,7 @@ type Superuser struct {
}

func (*Superuser) CanAccessSite(action rbac.Action) bool { return true }
func (*Superuser) CanAccessTeam(rbac.Action, string) bool { return true }
func (*Superuser) CanAccessOrganization(rbac.Action, string) bool { return true }
func (*Superuser) CanAccessWorkspace(rbac.Action, WorkspacePolicy) bool { return true }
func (s *Superuser) Organizations() []string { return nil }
Expand Down
2 changes: 1 addition & 1 deletion internal/organization/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (s *service) UpdateOrganization(ctx context.Context, name string, opts Upda
// Subject is a user: list their organization memberships
// Subject is an agent: return its organization
// Subject is an organization token: return its organization
// Subject is an team token: return its organization
// Subject is a team: return its organization
func (s *service) ListOrganizations(ctx context.Context, opts ListOptions) (*resource.Page[*Organization], error) {
subject, err := internal.SubjectFromContext(ctx)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions internal/rbac/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const (

CreateRunTokenAction

CreateTeamTokenAction
GetTeamTokenAction
DeleteTeamTokenAction

CreateModuleAction
CreateModuleVersionAction
UpdateModuleAction
Expand Down
Loading

0 comments on commit 1e4b173

Please sign in to comment.