Skip to content

Commit

Permalink
enhance: better permissions support
Browse files Browse the repository at this point in the history
  • Loading branch information
plyr4 committed Nov 7, 2024
1 parent 8e0fe7f commit 9355606
Show file tree
Hide file tree
Showing 15 changed files with 466 additions and 153 deletions.
3 changes: 2 additions & 1 deletion cmd/vela-server/scm.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ func setupSCM(c *cli.Context, tc *tracing.Client) (scm.Service, error) {
ClientSecret: c.String("scm.secret"),
AppID: c.Int64("scm.app.id"),
AppPrivateKey: c.String("scm.app.private_key"),
AppPermissions: c.StringSlice("scm.app.permissions"),
ServerAddress: c.String("server-addr"),
ServerWebhookAddress: c.String("scm.webhook.addr"),
StatusContext: c.String("scm.context"),
WebUIAddress: c.String("webui-addr"),
Scopes: c.StringSlice("scm.scopes"),
OAuthScopes: c.StringSlice("scm.scopes"),
Tracing: tc,
}

Expand Down
21 changes: 0 additions & 21 deletions constants/app_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,11 @@
// App Install vars.
package constants

// see: https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28
const (
// GitHub App install permission 'none'.
AppInstallPermissionNone = "none"
// GitHub App install permission 'read'.
AppInstallPermissionRead = "read"
// GitHub App install permission 'write'.
AppInstallPermissionWrite = "write"
)

const (
// GitHub App install contents resource.
AppInstallResourceContents = "contents"
// GitHub App install checks resource.
AppInstallResourceChecks = "checks"
)

const (
// GitHub App install repositories selection when "all" repositories are selected.
AppInstallRepositoriesSelectionAll = "all"
// GitHub App install repositories selection when a subset of repositories are selected.
AppInstallRepositoriesSelectionSelected = "selected"
// GitHub App install event type 'added'.
AppInstallRepositoriesAdded = "added"
// GitHub App install event type 'removed'.
AppInstallRepositoriesRemoved = "removed"
)

const (
Expand Down
7 changes: 7 additions & 0 deletions scm/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,11 @@ var Flags = []cli.Flag{
Name: "scm.app.private_key",
Usage: "set value of base64 encoded SCM App integration (GitHub App) private key",
},
&cli.StringSliceFlag{
EnvVars: []string{"VELA_SCM_APP_PERMISSIONS", "SCM_APP_PERMISSIONS", "VELA_SOURCE_APP_PERMISSIONS", "SOURCE_APP_PERMISSIONS"},
FilePath: "/vela/scm/app/permissions",
Name: "scm.app.permissions",
Usage: "SCM App integration (GitHub App) permissions to be used as the allowed set of possible installation token permissions",
Value: cli.NewStringSlice("contents:read", "checks:write"),
},
}
104 changes: 104 additions & 0 deletions scm/github/app_permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: Apache-2.0

package github

import (
"fmt"
"strings"

"github.com/google/go-github/v65/github"
)

// see: https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28
const (
// GitHub App install permission 'none'.
AppInstallPermissionNone = "none"
// GitHub App install permission 'read'.
AppInstallPermissionRead = "read"
// GitHub App install permission 'write'.
AppInstallPermissionWrite = "write"
)

const (
// GitHub App install contents resource.
AppInstallResourceContents = "contents"
// GitHub App install checks resource.
AppInstallResourceChecks = "checks"
// GitHub App install packages resource.
AppInstallResourcePackages = "packages"
// add more supported resources as needed.
)

// GetInstallationPermission takes permissions and returns the permission level if valid.
func GetInstallationPermission(resource string, appPermissions *github.InstallationPermissions) (string, error) {
switch resource {
case AppInstallResourceContents:
return appPermissions.GetContents(), nil
case AppInstallResourceChecks:
return appPermissions.GetChecks(), nil
case AppInstallResourcePackages:
return appPermissions.GetPackages(), nil
// add more supported resources as needed.
default:
return "", fmt.Errorf("given permission resource not supported: %s", resource)
}
}

// ApplyInstallationPermissions takes permissions and applies a new permission if valid.
func ApplyInstallationPermissions(resource, perm string, perms *github.InstallationPermissions) (*github.InstallationPermissions, error) {
// convert permissions from string
switch strings.ToLower(perm) {
case AppInstallPermissionNone:
case AppInstallPermissionRead:
case AppInstallPermissionWrite:
break
default:
return perms, fmt.Errorf("invalid permission level given for <resource>:<level> in %s:%s", resource, perm)
}

// convert resource from string
switch strings.ToLower(resource) {
case AppInstallResourceContents:
perms.Contents = github.String(perm)
case AppInstallResourceChecks:
perms.Checks = github.String(perm)
case AppInstallResourcePackages:
perms.Packages = github.String(perm)
// add more supported resources as needed.
default:
return perms, fmt.Errorf("invalid permission resource given for <resource>:<level> in %s:%s", resource, perm)
}

return perms, nil
}

// InstallationHasPermission takes a resource:perm pair and checks if the actual permission matches the expected permission or is supersceded by a higher permission.
func InstallationHasPermission(resource, requiredPerm, actualPerm string) error {
if len(actualPerm) == 0 {
return fmt.Errorf("github app missing permission %s:%s", resource, requiredPerm)
}

permitted := false

switch requiredPerm {
case AppInstallPermissionNone:
permitted = true
case AppInstallPermissionRead:
if actualPerm == AppInstallPermissionRead ||
actualPerm == AppInstallPermissionWrite {
permitted = true
}
case AppInstallPermissionWrite:
if actualPerm == AppInstallPermissionWrite {
permitted = true
}
default:
return fmt.Errorf("invalid required permission type: %s", requiredPerm)
}

if !permitted {
return fmt.Errorf("github app requires permission %s:%s, found: %s", AppInstallResourceContents, AppInstallPermissionRead, actualPerm)
}

return nil
}
195 changes: 195 additions & 0 deletions scm/github/app_permissions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// SPDX-License-Identifier: Apache-2.0

package github

import (
"testing"

"github.com/google/go-github/v65/github"
)

func TestGetInstallationPermission(t *testing.T) {
tests := []struct {
name string
resource string
permissions *github.InstallationPermissions
expectedPerm string
expectedError bool
}{
{
name: "valid contents permission",
resource: AppInstallResourceContents,
permissions: &github.InstallationPermissions{Contents: github.String(AppInstallPermissionRead)},
expectedPerm: AppInstallPermissionRead,
},
{
name: "valid checks permission",
resource: AppInstallResourceChecks,
permissions: &github.InstallationPermissions{Checks: github.String(AppInstallPermissionWrite)},
expectedPerm: AppInstallPermissionWrite,
},
{
name: "valid packages permission",
resource: AppInstallResourcePackages,
permissions: &github.InstallationPermissions{Packages: github.String(AppInstallPermissionNone)},
expectedPerm: AppInstallPermissionNone,
},
{
name: "invalid resource",
resource: "invalid_resource",
permissions: &github.InstallationPermissions{},
expectedError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
perm, err := GetInstallationPermission(tt.resource, tt.permissions)
if (err != nil) != tt.expectedError {
t.Errorf("GetInstallationPermission() error = %v, expectedError %v", err, tt.expectedError)
return
}
if perm != tt.expectedPerm {
t.Errorf("GetInstallationPermission() = %v, expected %v", perm, tt.expectedPerm)
}
})
}
}

func TestApplyInstallationPermissions(t *testing.T) {
tests := []struct {
name string
resource string
perm string
initialPerms *github.InstallationPermissions
expectedPerms *github.InstallationPermissions
expectedError bool
}{
{
name: "apply read permission to contents",
resource: AppInstallResourceContents,
perm: AppInstallPermissionRead,
initialPerms: &github.InstallationPermissions{},
expectedPerms: &github.InstallationPermissions{
Contents: github.String(AppInstallPermissionRead),
},
},
{
name: "apply write permission to checks",
resource: AppInstallResourceChecks,
perm: AppInstallPermissionWrite,
initialPerms: &github.InstallationPermissions{},
expectedPerms: &github.InstallationPermissions{
Checks: github.String(AppInstallPermissionWrite),
},
},
{
name: "apply none permission to packages",
resource: AppInstallResourcePackages,
perm: AppInstallPermissionNone,
initialPerms: &github.InstallationPermissions{},
expectedPerms: &github.InstallationPermissions{
Packages: github.String(AppInstallPermissionNone),
},
},
{
name: "invalid permission level",
resource: AppInstallResourceContents,
perm: "invalid_perm",
initialPerms: &github.InstallationPermissions{},
expectedError: true,
},
{
name: "invalid resource",
resource: "invalid_resource",
perm: AppInstallPermissionRead,
initialPerms: &github.InstallationPermissions{},
expectedError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
perms, err := ApplyInstallationPermissions(tt.resource, tt.perm, tt.initialPerms)
if (err != nil) != tt.expectedError {
t.Errorf("ApplyInstallationPermissions() error = %v, expectedError %v", err, tt.expectedError)
return
}
if !tt.expectedError && !comparePermissions(perms, tt.expectedPerms) {
t.Errorf("ApplyInstallationPermissions() = %v, expected %v", perms, tt.expectedPerms)
}
})
}
}

func TestInstallationHasPermission(t *testing.T) {
tests := []struct {
name string
resource string
requiredPerm string
actualPerm string
expectedError bool
}{
{
name: "valid read permission",
resource: AppInstallResourceContents,
requiredPerm: AppInstallPermissionRead,
actualPerm: AppInstallPermissionRead,
},
{
name: "valid write permission",
resource: AppInstallResourceChecks,
requiredPerm: AppInstallPermissionWrite,
actualPerm: AppInstallPermissionWrite,
},
{
name: "valid none permission",
resource: AppInstallResourcePackages,
requiredPerm: AppInstallPermissionNone,
actualPerm: AppInstallPermissionNone,
},
{
name: "read permission superseded by write",
resource: AppInstallResourceContents,
requiredPerm: AppInstallPermissionRead,
actualPerm: AppInstallPermissionWrite,
},
{
name: "missing permission",
resource: AppInstallResourceChecks,
requiredPerm: AppInstallPermissionWrite,
actualPerm: "",
expectedError: true,
},
{
name: "invalid required permission",
resource: AppInstallResourcePackages,
requiredPerm: "invalid_perm",
actualPerm: AppInstallPermissionRead,
expectedError: true,
},
{
name: "insufficient permission",
resource: AppInstallResourceContents,
requiredPerm: AppInstallPermissionWrite,
actualPerm: AppInstallPermissionRead,
expectedError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := InstallationHasPermission(tt.resource, tt.requiredPerm, tt.actualPerm)
if (err != nil) != tt.expectedError {
t.Errorf("InstallationHasPermission() error = %v, expectedError %v", err, tt.expectedError)
}
})
}
}

func comparePermissions(a, b *github.InstallationPermissions) bool {
if a == nil || b == nil {
return a == b
}
return github.Stringify(a) == github.Stringify(b)
}
Loading

0 comments on commit 9355606

Please sign in to comment.