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

Feature Allowlist in config #745

Merged
merged 10 commits into from
Nov 6, 2023
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
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