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

Add Spinnaker application permissions #22

Merged
merged 2 commits into from
Jul 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions spinnaker/api/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ var (
"tencentcloud": {50, `^[a-zA-Z_0-9.\u4e00-\u9fa5-]*$`},
"titus": {250, `^[a-zA-Z_0-9.]*$`},
}

// SupportedAccesses is a list for Spinnaker application level
// See details here
// ref: https://spinnaker.io/setup/security/authorization/
SupportedAccesses = []string{"READ", "WRITE", "EXECUTE"}
)

// applicationNameConstraint ...
Expand Down Expand Up @@ -61,6 +66,39 @@ func NewCreateApplicationTask(d *schema.ResourceData) (CreateApplicationTask, er
app["cloudProviders"] = strings.Join(cloudProviders, ",")
}

if v, ok := d.GetOkExists("permission"); ok {
var permissions = map[string][]string{}

inputs := v.([]interface{})
for _, input := range inputs {
input := input.(map[string]interface{})
accesses := convToStringArray(input["accesses"].([]interface{}))
if err := validateSpinnakerApplicationAccess(accesses); err != nil {
return nil, err
}
for _, access := range accesses {
if user := input["user"].(string); user != "" {
if len(permissions[access]) == 0 {
permissions[access] = []string{user}
continue
}

if users := permissions[access]; len(users) > 0{
for _, v := range users {
if user == v {
return nil, fmt.Errorf("user %s permission's declare duplicated", user)
}
}
}

permissions[access] = append(permissions[access], user)
}
}
}

app["permissions"] = permissions
}

createAppTask := map[string]interface{}{
"job": []interface{}{map[string]interface{}{"type": "createApplication", "application": app}},
"application": app["name"],
Expand Down Expand Up @@ -202,3 +240,31 @@ func validateSpinnakerApplicationNameByCloudProvider(appName, provider string) e

return fmt.Errorf("cloud provider %s is not supported", provider)
}

func validateSpinnakerApplicationAccess(accesses []string) error {
for _, access := range accesses {
var validAccess bool
for _, v := range SupportedAccesses {
if access == v {
validAccess = true
}
}

if !validAccess {
return fmt.Errorf("access %s is not supported", access)
}
}

return nil
}

func convToStringArray(in []interface{}) []string {
out := make([]string, len(in))
for i, v := range in {
if str, ok := v.(string); ok {
out[i] = str
}
}

return out
}
24 changes: 24 additions & 0 deletions spinnaker/api/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ func TestValidateApplicationCloudProviders(t *testing.T) {
}

for n, tc := range tcs {
tc := tc
t.Run(n, func(t *testing.T) {
t.Parallel()
for _, p := range tc.cloudProviders {
err := validateSpinnakerApplicationNameByCloudProvider(tc.appName, p)
if err != nil && tc.shouldPass {
Expand All @@ -28,3 +30,25 @@ func TestValidateApplicationCloudProviders(t *testing.T) {
})
}
}

func TestValidateSpinnakerApplicationAccess(t *testing.T) {
tcs := map[string]struct {
accesses []string
shouldPass bool
}{
"pass": {[]string{"WRITE"}, true},
"pass with multiple access": {SupportedAccesses, true},
"not supported access": {[]string{"MERCARI", "KEKE"}, false},
"mixture of supported access and not supported access": {[]string{"WRITE", "KEKE", "BLOG"}, false},
}

for n, tc := range tcs {
tc := tc
t.Run(n, func(t *testing.T) {
t.Parallel()
if err := validateSpinnakerApplicationAccess(tc.accesses); err != nil && tc.shouldPass {
t.Fatalf("failed: %v", err)
}
})
}
}
4 changes: 4 additions & 0 deletions spinnaker/data_source_spinnaker_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ func datasourceApplication() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"permission": {
Type: schema.TypeMap,
Computed: true,
},
},
Read: resourceSpinnakerApplicationRead,
}
Expand Down
1 change: 1 addition & 0 deletions spinnaker/data_source_spinnaker_application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func TestAccDataSourceSpinnakerApplication_cloudProviders(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "name", rName),
resource.TestCheckResourceAttr(resourceName, "email", "[email protected]"),
resource.TestCheckResourceAttr(resourceName, "instance_port", strconv.Itoa(defaultInstancePort)),
resource.TestCheckResourceAttr(resourceName, "cloud_providers", cloudProvider),
),
},
},
Expand Down
72 changes: 66 additions & 6 deletions spinnaker/resource_application.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package spinnaker

import (
"fmt"
"strings"

"github.com/hashicorp/terraform/helper/schema"
Expand Down Expand Up @@ -39,6 +40,14 @@ func resourceSpinnakerApplication() *schema.Resource {
Optional: true,
Default: defaultInstancePort,
},
"permission": {
Description: "Application level permissions",
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: getPermissionSchema(),
},
},
},
Create: resourceSpinnakerApplicationCreate,
Read: resourceSpinnakerApplicationRead,
Expand All @@ -57,10 +66,17 @@ type applicationRead struct {
}

type applicationAttributes struct {
Accounts string `json:"accounts"`
CloudProviders string `json:"cloudproviders"`
Email string `json:"email"`
InstancePort int `json:"instancePort"`
Accounts string `json:"accounts"`
CloudProviders string `json:"cloudproviders"`
Email string `json:"email"`
InstancePort int `json:"instancePort"`
Permissions *Permissions `json:"permissions"`
}

type Permissions struct {
Read []string `json:"READ"`
Execute []string `json:"EXECUTE"`
Write []string `json:"WRITE"`
}

func resourceSpinnakerApplicationCreate(d *schema.ResourceData, meta interface{}) error {
Expand Down Expand Up @@ -108,6 +124,14 @@ func resourceSpinnakerApplicationRead(d *schema.ResourceData, meta interface{})
if v := app.Attributes.InstancePort; v != 0 {
d.Set("instance_port", v)
}
if v := app.Attributes.Permissions; v != nil {
terraformPermissions, err := buildTerraformPermissions(v)
if err != nil {
return err
}

d.Set("permissions", terraformPermissions)
}

return nil
}
Expand Down Expand Up @@ -167,6 +191,42 @@ func resourceSpinnakerApplicationImport(d *schema.ResourceData, meta interface{}
return []*schema.ResourceData{d}, nil
}

func flattenCloudProviders(input string) []string {
return strings.Split(input, ",")
func getPermissionSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
"user": {
Type: schema.TypeString,
Description: "User ID",
Required: true,
},
"accesses": {
Type: schema.TypeList,
Description: "List of access",
Elem: &schema.Schema{Type: schema.TypeString},
Required: true,
},
}
}

func buildTerraformPermissions(permissions *Permissions) (*map[string][]string, error) {
users := map[string][]string{}
for _, rUser := range permissions.Read {
users[rUser] = append(users[rUser], "READ")
}

for _, xUser := range permissions.Execute {
users[xUser] = append(users[xUser], "EXECUTE")
}

for _, wUser := range permissions.Read {
users[wUser] = append(users[wUser], "WRITE")
}

for user, accesses := range users {
if len(accesses) > 3 {
return nil, fmt.Errorf("more than 3 access granted for %s", user)
}
}

return &users, nil
}

1 change: 1 addition & 0 deletions spinnaker/validators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func TestValidateApplicationName(t *testing.T) {
"invalid_name",
"",
}

for _, v := range invalidNames {
_, errors := validateSpinnakerApplicationName(v, "application")
if len(errors) == 0 {
Expand Down
4 changes: 4 additions & 0 deletions website/docs/d/application.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ data "spinnaker_application" "my_app" {}
* `last_modified_by` - ID of the user last modified
* `name` - Name of the user
* `user` - User associated to application
* `permissions` - List of application level permissions
* `read` - List of `READ` permission's users, teams
* `execute` - List of `EXECUTE` permission's user, teams
* `write` - List of `WRITE` permission's users, teams
12 changes: 11 additions & 1 deletion website/docs/r/application.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,23 @@ resource "spinnaker_application" "my_app" {

## Argument Reference

The following arguments are supported:
~> **Be careful!** You can accidentally lock yourself out of your Spinnaker application using `permission` attribute. One user or team should obtain `write` permission to edit the application after creation.

The following arguments are supported.

* `application` - (Required) The Name of the application.
* `email` - (Required) Email of the owner.
* `cloud_providers` - (Optional) List of the cloud providers.
* `instance_port` - (Optional) Port of the Spinnaker generated links. Default to `80`.
* `permission` - (Optional) Nested block describing a application permission configuration. You have to enable [Authorization(RBAC)](https://spinnaker.io/setup/security/authorization/) for your Spinnaker to use this feature.

### Nested `permission` block

Nested `permission` block will have the following structure:

* `user` - (Required) ID of the user. The ID type depends on the authorization methods. For example, the ID will be the email address if you use G Suite. Also, if you use GitHub Teams the ID will be the team name.
* `accesses` - (Required) List of the access permission. The options are `READ`, `EXECUTE` and `WRITE`.

## Import

Applications can be imported using their Spinnaker application name, e.g.
Expand Down