Skip to content

Commit f32dd4e

Browse files
committed
Add ability to delegate authorization to external sources
1 parent ee1134f commit f32dd4e

18 files changed

+408
-80
lines changed

server/core/config/parser_validator_test.go

+31-1
Original file line numberDiff line numberDiff line change
@@ -1326,10 +1326,13 @@ func TestParseGlobalCfg(t *testing.T) {
13261326
},
13271327
},
13281328
Workflows: defaultCfg.Workflows,
1329+
TeamAuthz: valid.TeamAuthz{
1330+
Args: make([]string, 0),
1331+
},
13291332
},
13301333
},
13311334
"disable repo locks": {
1332-
input: `repos:
1335+
input: `repos:
13331336
- id: /.*/
13341337
repo_locks:
13351338
mode: disabled`,
@@ -1342,6 +1345,9 @@ func TestParseGlobalCfg(t *testing.T) {
13421345
},
13431346
},
13441347
Workflows: defaultCfg.Workflows,
1348+
TeamAuthz: valid.TeamAuthz{
1349+
Args: make([]string, 0),
1350+
},
13451351
},
13461352
},
13471353
"no workflows key": {
@@ -1362,6 +1368,9 @@ workflows:
13621368
"default": defaultCfg.Workflows["default"],
13631369
"name": defaultWorkflow("name"),
13641370
},
1371+
TeamAuthz: valid.TeamAuthz{
1372+
Args: make([]string, 0),
1373+
},
13651374
},
13661375
},
13671376
"workflow stages empty": {
@@ -1380,6 +1389,9 @@ workflows:
13801389
"default": defaultCfg.Workflows["default"],
13811390
"name": defaultWorkflow("name"),
13821391
},
1392+
TeamAuthz: valid.TeamAuthz{
1393+
Args: make([]string, 0),
1394+
},
13831395
},
13841396
},
13851397
"workflow steps empty": {
@@ -1403,6 +1415,9 @@ workflows:
14031415
"default": defaultCfg.Workflows["default"],
14041416
"name": defaultWorkflow("name"),
14051417
},
1418+
TeamAuthz: valid.TeamAuthz{
1419+
Args: make([]string, 0),
1420+
},
14061421
},
14071422
},
14081423
"all keys specified": {
@@ -1509,6 +1524,9 @@ policies:
15091524
},
15101525
},
15111526
},
1527+
TeamAuthz: valid.TeamAuthz{
1528+
Args: make([]string, 0),
1529+
},
15121530
},
15131531
},
15141532
"id regex with trailing slash": {
@@ -1526,6 +1544,9 @@ repos:
15261544
Workflows: map[string]valid.Workflow{
15271545
"default": defaultCfg.Workflows["default"],
15281546
},
1547+
TeamAuthz: valid.TeamAuthz{
1548+
Args: make([]string, 0),
1549+
},
15291550
},
15301551
},
15311552
"referencing default workflow": {
@@ -1545,6 +1566,9 @@ repos:
15451566
Workflows: map[string]valid.Workflow{
15461567
"default": defaultCfg.Workflows["default"],
15471568
},
1569+
TeamAuthz: valid.TeamAuthz{
1570+
Args: make([]string, 0),
1571+
},
15481572
},
15491573
},
15501574
"redefine default workflow": {
@@ -1620,6 +1644,9 @@ workflows:
16201644
},
16211645
},
16221646
},
1647+
TeamAuthz: valid.TeamAuthz{
1648+
Args: make([]string, 0),
1649+
},
16231650
},
16241651
},
16251652
}
@@ -1841,6 +1868,9 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
18411868
},
18421869
},
18431870
},
1871+
TeamAuthz: valid.TeamAuthz{
1872+
Args: make([]string, 0),
1873+
},
18441874
},
18451875
},
18461876
}

server/core/config/raw/global_cfg.go

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type GlobalCfg struct {
1717
Workflows map[string]Workflow `yaml:"workflows" json:"workflows"`
1818
PolicySets PolicySets `yaml:"policies" json:"policies"`
1919
Metrics Metrics `yaml:"metrics" json:"metrics"`
20+
TeamAuthz TeamAuthz `yaml:"team_authz" json:"team_authz"`
2021
}
2122

2223
// Repo is the raw schema for repos in the server-side repo config.
@@ -161,6 +162,7 @@ func (g GlobalCfg) ToValid(defaultCfg valid.GlobalCfg) valid.GlobalCfg {
161162
Workflows: workflows,
162163
PolicySets: g.PolicySets.ToValid(),
163164
Metrics: g.Metrics.ToValid(),
165+
TeamAuthz: g.TeamAuthz.ToValid(),
164166
}
165167
}
166168

server/core/config/raw/team_authz.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package raw
2+
3+
import "github.com/runatlantis/atlantis/server/core/config/valid"
4+
5+
type TeamAuthz struct {
6+
Command string `yaml:"command" json:"command"`
7+
Args []string `yaml:"args" json:"args"`
8+
}
9+
10+
func (t *TeamAuthz) ToValid() valid.TeamAuthz {
11+
var v valid.TeamAuthz
12+
v.Command = t.Command
13+
v.Args = make([]string, 0)
14+
if t.Args != nil {
15+
for _, arg := range t.Args {
16+
v.Args = append(v.Args, arg)
17+
}
18+
}
19+
20+
return v
21+
}

server/core/config/valid/global_cfg.go

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type GlobalCfg struct {
4747
Workflows map[string]Workflow
4848
PolicySets PolicySets
4949
Metrics Metrics
50+
TeamAuthz TeamAuthz
5051
}
5152

5253
type Metrics struct {
@@ -249,6 +250,9 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg {
249250
Workflows: map[string]Workflow{
250251
DefaultWorkflowName: defaultWorkflow,
251252
},
253+
TeamAuthz: TeamAuthz{
254+
Args: make([]string, 0),
255+
},
252256
}
253257
}
254258

server/core/config/valid/global_cfg_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ func TestNewGlobalCfg(t *testing.T) {
8989
Workflows: map[string]valid.Workflow{
9090
"default": expDefaultWorkflow,
9191
},
92+
TeamAuthz: valid.TeamAuthz{
93+
Args: make([]string, 0),
94+
},
9295
}
9396

9497
cases := []struct {
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package valid
2+
3+
type TeamAuthz struct {
4+
Command string `yaml:"command" json:"command"`
5+
Args []string `yaml:"args" json:"args"`
6+
}

server/events/command/context.go

+3
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,7 @@ type Context struct {
4646

4747
// API is true if plan/apply by API endpoints
4848
API bool
49+
50+
// TeamAllowlistChecker is used to check authorization on a project-level
51+
TeamAllowlistChecker TeamAllowlistChecker
4952
}

server/events/command/project_context.go

+3
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ type ProjectContext struct {
126126
// Allows custom policy check tools outside of Conftest to run in checks
127127
CustomPolicyCheck bool
128128
SilencePRComments []string
129+
130+
// TeamAllowlistChecker is used to check authorization on a project-level
131+
TeamAllowlistChecker TeamAllowlistChecker
129132
}
130133

131134
// SetProjectScopeTags adds ProjectContext tags to a new returned scope.

server/events/team_allowlist_checker.go server/events/command/team_allowlist_checker.go

+21-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
package events
1+
package command
22

33
import (
4+
"github.com/runatlantis/atlantis/server/events/models"
45
"strings"
56
)
67

@@ -10,14 +11,25 @@ const wildcard = "*"
1011
// mapOfStrings is an alias for map[string]string
1112
type mapOfStrings map[string]string
1213

13-
// TeamAllowlistChecker implements checking the teams and the operations that the members
14+
type TeamAllowlistChecker interface {
15+
// HasRules returns true if the checker has rules defined
16+
HasRules() bool
17+
18+
// IsCommandAllowedForTeam determines if the specified team can perform the specified action
19+
IsCommandAllowedForTeam(ctx models.TeamAllowlistCheckerContext, team, command string) bool
20+
21+
// IsCommandAllowedForAnyTeam determines if any of the specified teams can perform the specified action
22+
IsCommandAllowedForAnyTeam(ctx models.TeamAllowlistCheckerContext, teams []string, command string) bool
23+
}
24+
25+
// DefaultTeamAllowlistChecker implements checking the teams and the operations that the members
1426
// of a particular team are allowed to perform
15-
type TeamAllowlistChecker struct {
27+
type DefaultTeamAllowlistChecker struct {
1628
rules []mapOfStrings
1729
}
1830

1931
// NewTeamAllowlistChecker constructs a new checker
20-
func NewTeamAllowlistChecker(allowlist string) (*TeamAllowlistChecker, error) {
32+
func NewTeamAllowlistChecker(allowlist string) (*DefaultTeamAllowlistChecker, error) {
2133
var rules []mapOfStrings
2234
pairs := strings.Split(allowlist, ",")
2335
if pairs[0] != "" {
@@ -29,18 +41,18 @@ func NewTeamAllowlistChecker(allowlist string) (*TeamAllowlistChecker, error) {
2941
rules = append(rules, m)
3042
}
3143
}
32-
return &TeamAllowlistChecker{
44+
return &DefaultTeamAllowlistChecker{
3345
rules: rules,
3446
}, nil
3547
}
3648

37-
func (checker *TeamAllowlistChecker) HasRules() bool {
49+
func (checker *DefaultTeamAllowlistChecker) HasRules() bool {
3850
return len(checker.rules) > 0
3951
}
4052

4153
// IsCommandAllowedForTeam returns true if the team is allowed to execute the command
4254
// and false otherwise.
43-
func (checker *TeamAllowlistChecker) IsCommandAllowedForTeam(team string, command string) bool {
55+
func (checker *DefaultTeamAllowlistChecker) IsCommandAllowedForTeam(_ models.TeamAllowlistCheckerContext, team string, command string) bool {
4456
for _, rule := range checker.rules {
4557
for key, value := range rule {
4658
if (key == wildcard || strings.EqualFold(key, team)) && (value == wildcard || strings.EqualFold(value, command)) {
@@ -53,7 +65,7 @@ func (checker *TeamAllowlistChecker) IsCommandAllowedForTeam(team string, comman
5365

5466
// IsCommandAllowedForAnyTeam returns true if any of the teams is allowed to execute the command
5567
// and false otherwise.
56-
func (checker *TeamAllowlistChecker) IsCommandAllowedForAnyTeam(teams []string, command string) bool {
68+
func (checker *DefaultTeamAllowlistChecker) IsCommandAllowedForAnyTeam(ctx models.TeamAllowlistCheckerContext, teams []string, command string) bool {
5769
if len(teams) == 0 {
5870
for _, rule := range checker.rules {
5971
for key, value := range rule {
@@ -64,7 +76,7 @@ func (checker *TeamAllowlistChecker) IsCommandAllowedForAnyTeam(teams []string,
6476
}
6577
} else {
6678
for _, t := range teams {
67-
if checker.IsCommandAllowedForTeam(t, command) {
79+
if checker.IsCommandAllowedForTeam(ctx, t, command) {
6880
return true
6981
}
7082
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package command_test
2+
3+
import (
4+
"github.com/runatlantis/atlantis/server/events/command"
5+
"github.com/runatlantis/atlantis/server/events/models"
6+
"testing"
7+
8+
. "github.com/runatlantis/atlantis/testing"
9+
)
10+
11+
func TestNewTeamAllowListChecker(t *testing.T) {
12+
allowlist := `bob:plan, dave:apply`
13+
_, err := command.NewTeamAllowlistChecker(allowlist)
14+
Ok(t, err)
15+
}
16+
17+
func TestNewTeamAllowListCheckerEmpty(t *testing.T) {
18+
allowlist := ``
19+
checker, err := command.NewTeamAllowlistChecker(allowlist)
20+
Ok(t, err)
21+
Equals(t, false, checker.HasRules())
22+
}
23+
24+
func TestIsCommandAllowedForTeam(t *testing.T) {
25+
allowlist := `bob:plan, dave:apply, connie:plan, connie:apply`
26+
checker, err := command.NewTeamAllowlistChecker(allowlist)
27+
Ok(t, err)
28+
Equals(t, true, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, "connie", "plan"))
29+
Equals(t, true, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, "connie", "apply"))
30+
Equals(t, true, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, "dave", "apply"))
31+
Equals(t, true, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, "bob", "plan"))
32+
Equals(t, false, checker.IsCommandAllowedForTeam(models.TeamAllowlistCheckerContext{}, "bob", "apply"))
33+
}
34+
35+
func TestIsCommandAllowedForAnyTeam(t *testing.T) {
36+
allowlist := `alpha:plan,beta:release,*:unlock,nobody:*`
37+
teams := []string{`alpha`, `beta`}
38+
checker, err := command.NewTeamAllowlistChecker(allowlist)
39+
Ok(t, err)
40+
Equals(t, true, checker.IsCommandAllowedForAnyTeam(models.TeamAllowlistCheckerContext{}, teams, `plan`))
41+
Equals(t, true, checker.IsCommandAllowedForAnyTeam(models.TeamAllowlistCheckerContext{}, teams, `release`))
42+
Equals(t, true, checker.IsCommandAllowedForAnyTeam(models.TeamAllowlistCheckerContext{}, teams, `unlock`))
43+
Equals(t, false, checker.IsCommandAllowedForAnyTeam(models.TeamAllowlistCheckerContext{}, teams, `noop`))
44+
}

0 commit comments

Comments
 (0)