Skip to content

Commit

Permalink
Feature Allowlist in config (#745)
Browse files Browse the repository at this point in the history
* Add AllowList to config

* Rename isPackageExcluded to be generic

* Not ! in list

* Add tests and fix issues

* Use allow package in server

* Not rename method

* Remove lineshift

* Add allow list in config example

* Fix typo server_handler.go

* Fix typo
  • Loading branch information
olekenneth authored Nov 6, 2023
1 parent 8b0300b commit 4c8e077
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 31 deletions.
8 changes: 8 additions & 0 deletions config.example.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,13 @@
"package_name"
]
}]
},

// The list to only allow some packages or scopes.
"allowList": {
"packages": ["@some_scope/package_name"],
"scopes": [{
"name": "@your_scope",
}]
}
}
101 changes: 71 additions & 30 deletions server/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,27 @@ import (
const MinBuildConcurrency = 4

type Config struct {
Port uint16 `json:"port,omitempty"`
TlsPort uint16 `json:"tlsPort,omitempty"`
NsPort uint16 `json:"nsPort,omitempty"`
BuildConcurrency uint16 `json:"buildConcurrency,omitempty"`
BanList BanList `json:"banList,omitempty"`
AuthSecret string `json:"authSecret,omitempty"`
WorkDir string `json:"workDir,omitempty"`
Cache string `json:"cache,omitempty"`
Database string `json:"database,omitempty"`
Storage string `json:"storage,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
LogDir string `json:"logDir,omitempty"`
CdnOrigin string `json:"cdnOrigin,omitempty"`
CdnBasePath string `json:"cdnBasePath,omitempty"`
NpmRegistry string `json:"npmRegistry,omitempty"`
NpmToken string `json:"npmToken,omitempty"`
NpmRegistryScope string `json:"npmRegistryScope,omitempty"`
NpmUser string `json:"npmUser,omitempty"`
NpmPassword string `json:"npmPassword,omitempty"`
NoCompress bool `json:"noCompress,omitempty"`
Port uint16 `json:"port,omitempty"`
TlsPort uint16 `json:"tlsPort,omitempty"`
NsPort uint16 `json:"nsPort,omitempty"`
BuildConcurrency uint16 `json:"buildConcurrency,omitempty"`
BanList BanList `json:"banList,omitempty"`
AllowList AllowList `json:"allowList,omitempty"`
AuthSecret string `json:"authSecret,omitempty"`
WorkDir string `json:"workDir,omitempty"`
Cache string `json:"cache,omitempty"`
Database string `json:"database,omitempty"`
Storage string `json:"storage,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
LogDir string `json:"logDir,omitempty"`
CdnOrigin string `json:"cdnOrigin,omitempty"`
CdnBasePath string `json:"cdnBasePath,omitempty"`
NpmRegistry string `json:"npmRegistry,omitempty"`
NpmToken string `json:"npmToken,omitempty"`
NpmRegistryScope string `json:"npmRegistryScope,omitempty"`
NpmUser string `json:"npmUser,omitempty"`
NpmPassword string `json:"npmPassword,omitempty"`
NoCompress bool `json:"noCompress,omitempty"`
}

type BanList struct {
Expand All @@ -48,6 +49,15 @@ type BanScope struct {
Excludes []string `json:"excludes"`
}

type AllowList struct {
Packages []string `json:"packages"`
Scopes []AllowScope `json:"scopes"`
}

type AllowScope struct {
Name string `json:"name"`
}

// Load loads config from the given file. Panic if failed to load.
func Load(filename string) (*Config, error) {
var (
Expand Down Expand Up @@ -188,16 +198,14 @@ func fixConfig(c *Config) *Config {
return c
}

// IsPackageBanned Checking if the package is banned.
// The `packages` list is the highest priority ban rule to match,
// so the `excludes` list in the `scopes` list won't take effect if the package is banned in `packages` list
func (banList *BanList) IsPackageBanned(fullName string) bool {
var (
fullNameWithoutVersion string // e.g. @github/faker
scope string // e.g. @github
nameWithoutVersionScope string // e.g. faker
)
paths := strings.Split(fullName, "/")
// extractPackageName Will take a packageName as input extract key
// parts and return them
//
// fullNameWithoutVersion e.g. @github/faker
// scope e.g. @github
// nameWithoutVersionScope e.g. faker
func extractPackageName(packageName string) (fullNameWithoutVersion string, scope string, nameWithoutVersionScope string) {
paths := strings.Split(packageName, "/")
if len(paths) < 2 {
// the package has no scope prefix
nameWithoutVersionScope = strings.Split(paths[0], "@")[0]
Expand All @@ -208,6 +216,15 @@ func (banList *BanList) IsPackageBanned(fullName string) bool {
fullNameWithoutVersion = fmt.Sprintf("%s/%s", scope, nameWithoutVersionScope)
}

return fullNameWithoutVersion, scope, nameWithoutVersionScope
}

// IsPackageBanned Checking if the package is banned.
// The `packages` list is the highest priority ban rule to match,
// so the `excludes` list in the `scopes` list won't take effect if the package is banned in `packages` list
func (banList *BanList) IsPackageBanned(fullName string) bool {
fullNameWithoutVersion, scope, nameWithoutVersionScope := extractPackageName(fullName)

for _, p := range banList.Packages {
if fullNameWithoutVersion == p {
return true
Expand All @@ -223,6 +240,30 @@ func (banList *BanList) IsPackageBanned(fullName string) bool {
return false
}

// IsPackageAllowed Checking if the package is allowed.
// The `packages` list is the highest priority allow rule to match,
// so the `includes` list in the `scopes` list won't take effect if the package is allowed in `packages` list
func (allowList *AllowList) IsPackageAllowed(fullName string) bool {
if len(allowList.Packages) == 0 && len(allowList.Scopes) == 0 {
return true
}
fullNameWithoutVersion, scope, _ := extractPackageName(fullName)

for _, p := range allowList.Packages {
if fullNameWithoutVersion == p {
return true
}
}

for _, s := range allowList.Scopes {
if scope == s.Name {
return true
}
}

return false
}

func isPackageExcluded(name string, excludes []string) bool {
for _, exclude := range excludes {
if name == exclude {
Expand Down
162 changes: 162 additions & 0 deletions server/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,168 @@ import (
"testing"
)

func TestAllowListAndBanList_IsPackageNotAllowedOrBanned(t *testing.T) {
type args struct {
fullName string
}
tests := []struct {
name string
allowList AllowList
banList BanList
args args
want bool
}{
{
name: "NoAllowOrBanListAllowAnything",
allowList: AllowList{},
banList: BanList{},
args: args{fullName: "[email protected]"},
want: false,
},
{
name: "AllowedScopeBannedScope",
allowList: AllowList{
Scopes: []AllowScope{{
Name: "@github",
}},
},
banList: BanList{
Scopes: []BanScope{{
Name: "@github",
}},
},
args: args{fullName: "@github/faker"},
want: true,
},
{
name: "AllowedScopeBannedPackage",
allowList: AllowList{
Scopes: []AllowScope{{
Name: "@github",
}},
},
banList: BanList{
Packages: []string{"@github/faker"},
},
args: args{fullName: "@github/faker"},
want: true,
},
{
name: "AllowedPackageBannedPackage",
allowList: AllowList{
Packages: []string{"@github/faker"},
},
banList: BanList{
Packages: []string{"faker"},
},
args: args{fullName: "faker"},
want: true,
},
{
name: "AllowedPackageBannedScope",
allowList: AllowList{
Packages: []string{"faker"},
},
banList: BanList{
Scopes: []BanScope{{
Name: "@github",
}},
},
args: args{fullName: "@github/faker"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// to simulate:
// if !pkgAllowed || pkgBanned {
// return rex.Status(403, "forbidden")
// }
packageName := tt.args.fullName

isAllowed := tt.allowList.IsPackageAllowed(packageName)
isBanned := tt.banList.IsPackageBanned(packageName)

if got := !isAllowed || isBanned; got != tt.want {
t.Errorf("isPackageNotAllowedOrBanned() = %v, want %v. %v isAllowed %v, %v isBanned %v", got, tt.want, packageName, isAllowed, packageName, isBanned)
}
})
}
}


func TestAllowList_IsPackageAllowed(t *testing.T) {
type args struct {
fullName string
}
tests := []struct {
name string
allowList AllowList
args args
want bool
}{
{
name: "NoAllowListAllowAnything",
allowList: AllowList{},
args: args{fullName: "[email protected]"},
want: true,
},
{
name: "AllowedByPackages",
allowList: AllowList{
Packages: []string{"faker"},
},
args: args{fullName: "faker"},
want: true,
},
{
name: "NotAllowedByPackages",
allowList: AllowList{
Packages: []string{"allowedPackageName"},
},
args: args{fullName: "faker"},
want: false,
},
{
name: "AllowedByScope",
allowList: AllowList{
Scopes: []AllowScope{{
Name: "@github",
}},
},
args: args{fullName: "@github/perfect"},
want: true,
},
{
name: "NotAllowedByScope",
allowList: AllowList{
Scopes: []AllowScope{{
Name: "@github",
}},
},
args: args{fullName: "@faker/perfect"},
want: false,
},
{
name: "NotAllowedByScope",
allowList: AllowList{
Scopes: []AllowScope{{
Name: "@github",
}},
},
args: args{fullName: "@faker/perfect"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.allowList.IsPackageAllowed(tt.args.fullName); got != tt.want {
t.Errorf("IsPackageAllowed() = %v, want %v", got, tt.want)
}
})
}
}

func TestBanList_IsPackageBanned(t *testing.T) {
type args struct {
fullName string
Expand Down
3 changes: 2 additions & 1 deletion server/server_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,8 +524,9 @@ func esmHandler() rex.Handle {
// trim the leading `/` in pathname to get the package name
// e.g. /@ORG/PKG -> @ORG/PKG
packageFullName := pathname[1:]
pkgAllowed := cfg.AllowList.IsPackageAllowed(packageFullName)
pkgBanned := cfg.BanList.IsPackageBanned(packageFullName)
if pkgBanned {
if !pkgAllowed || pkgBanned {
return rex.Status(403, "forbidden")
}

Expand Down

0 comments on commit 4c8e077

Please sign in to comment.