Skip to content

Commit

Permalink
api: Add libraries to Pascalify API endpoints
Browse files Browse the repository at this point in the history
We have various APIs defined in the Swagger specification, each of which
has a range of API endpoints like "GET /endpoint", "DELETE /ipam/{ip}",
etc. In order to make these endpoints easier to refer to in a consistent
manner, introduce new api library functions that consume a go-openapi
spec and generate PascalCase flags for each API. Add some additional
useful functions that can be used for the purpose of restricting the
accessibility of certain APIs based on this PascalCase configuration.

Signed-off-by: Joe Stringer <joe@cilium.io>
  • Loading branch information
joestringer committed Apr 19, 2023
1 parent 8544406 commit e8440eb
Show file tree
Hide file tree
Showing 2 changed files with 328 additions and 0 deletions.
137 changes: 137 additions & 0 deletions pkg/api/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package api

import (
"fmt"
"strings"

"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
)

var (
ErrUnknownWildcard = fmt.Errorf("Unsupported API wildcard")
ErrUnknownFlag = fmt.Errorf("Unknown API flag")
)

func pascalize(in string) string {
if len(in) < 2 {
return strings.ToUpper(in)
}
switch in {
case "bgp":
return "BGP"
case "id":
return "ID"
case "ip":
return "IP"
case "ipam":
return "IPAM"
case "lrp":
return "LRP"
}
return strings.ToUpper(in[0:1]) + strings.ToLower(in[1:])
}

func pathToFlagSuffix(path string) string {
result := ""
path = strings.TrimPrefix(path, "/")
for _, hunk := range strings.Split(path, "/") {
// TODO: Maybe we can just rename the /cgroup-dump-metadata API to /cgroups to avoid this loop?
for _, word := range strings.Split(hunk, "-") {
trimmed := strings.Trim(word, "{}")
result = result + pascalize(trimmed)
}
}

return result
}

func parseSpecPaths(paths *spec.Paths) PathSet {
results := make(PathSet)

for path, item := range paths.Paths {
suffix := pathToFlagSuffix(path)
ops := map[string]*spec.Operation{
"Delete": item.Delete,
"Get": item.Get,
"Patch": item.Patch,
"Post": item.Post,
"Put": item.Put,
}
for prefix, op := range ops {
if op != nil {
flag := prefix + suffix
results[flag] = Endpoint{
Method: strings.ToUpper(prefix),
Path: path,
Description: op.Description,
}
}
}
}

return PathSet(results)
}

func generateDeniedAPIEndpoints(allPaths PathSet, allowed []string) (PathSet, error) {
// default to "deny all", then allow specified APIs by flag
denied := allPaths

var wildcardPrefixes []string
for _, opt := range allowed {
switch strings.Index(opt, "*") {
case -1: // No wildcard
break
case len(opt) - 1: // suffix
prefix := strings.TrimSuffix(opt, "*")
if len(prefix) == 0 { // Full opt "*", ie allow all
return PathSet{}, nil
}
wildcardPrefixes = append(wildcardPrefixes, prefix)
continue
default:
return nil, fmt.Errorf("%w: %q", ErrUnknownWildcard, opt)
}
if _, ok := denied[opt]; ok {
delete(denied, opt)
} else {
return nil, fmt.Errorf("%w: %q", ErrUnknownFlag, opt)
}
}

for _, prefix := range wildcardPrefixes {
for f := range denied {
if strings.HasPrefix(f, prefix) {
delete(denied, f)
}
}
}
return denied, nil
}

// Endpoint is an API Endpoint for a parsed API specification.
type Endpoint struct {
Method string
Path string
Description string
}

// PathSet is a set of APIs in the form of a map of canonical pascalized flag
// name to MethodPath, for example:
// "GetEndpointID": {"GET", "/endpoint/{id}"}
type PathSet map[string]Endpoint

func NewPathSet(spec *loads.Document) PathSet {
return parseSpecPaths(spec.Spec().Paths)
}

// AllowedFlagsToDeniedPaths parses the input API specification and the provided
// commandline flags, and returns the PathSet that should be administratively
// disabled using a subsequent call to DisableAPIs().
func AllowedFlagsToDeniedPaths(spec *loads.Document, allowed []string) (PathSet, error) {
paths := parseSpecPaths(spec.Spec().Paths)
return generateDeniedAPIEndpoints(paths, allowed)
}
191 changes: 191 additions & 0 deletions pkg/api/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package api

import (
"errors"
"testing"

"github.com/go-openapi/spec"

"github.com/cilium/cilium/pkg/checker"
)

func TestParseSpecPaths(t *testing.T) {
testCases := [...]struct {
name string
paths *spec.Paths
expected PathSet
}{
{
name: "Basic GET BGP",
paths: &spec.Paths{Paths: map[string]spec.PathItem{
"/bgp": {PathItemProps: spec.PathItemProps{
Get: &spec.Operation{},
}}}},
expected: PathSet{
"GetBGP": {
Method: "GET",
Path: "/bgp",
}},
},
{
name: "PUT endpoints by ID",
paths: &spec.Paths{Paths: map[string]spec.PathItem{
"/endpoint/{id}": {PathItemProps: spec.PathItemProps{
Put: &spec.Operation{},
}}}},
expected: PathSet{
"PutEndpointID": {
Method: "PUT",
Path: "/endpoint/{id}",
}},
},
{
name: "DELETE LRP by ID with suffix",
paths: &spec.Paths{Paths: map[string]spec.PathItem{
"/lrp/{id}/foo": {PathItemProps: spec.PathItemProps{
Delete: &spec.Operation{},
}}}},
expected: PathSet{
"DeleteLRPIDFoo": {
Method: "DELETE",
Path: "/lrp/{id}/foo",
}},
},
{
name: "POST kebab-case",
paths: &spec.Paths{Paths: map[string]spec.PathItem{
"/cgroup-metadata-dump": {PathItemProps: spec.PathItemProps{
Post: &spec.Operation{},
}}}},
expected: PathSet{
"PostCgroupMetadataDump": {
Method: "POST",
Path: "/cgroup-metadata-dump",
}},
},
{
name: "Multiple endpoints PATCH and PUT",
paths: &spec.Paths{Paths: map[string]spec.PathItem{
"/endpoint/{id}": {PathItemProps: spec.PathItemProps{
Put: &spec.Operation{},
}},
"/endpoint/{id}/config": {PathItemProps: spec.PathItemProps{
Patch: &spec.Operation{},
}},
}},
expected: PathSet{
"PatchEndpointIDConfig": {
Method: "PATCH",
Path: "/endpoint/{id}/config",
},
"PutEndpointID": {
Method: "PUT",
Path: "/endpoint/{id}",
},
},
},
{
name: "Multiple methods PATCH and PUT ipam",
paths: &spec.Paths{Paths: map[string]spec.PathItem{
"/ipam/{ip}": {PathItemProps: spec.PathItemProps{
Put: &spec.Operation{},
Patch: &spec.Operation{},
}},
}},
expected: PathSet{
"PatchIPAMIP": {
Method: "PATCH",
Path: "/ipam/{ip}",
},
"PutIPAMIP": {
Method: "PUT",
Path: "/ipam/{ip}",
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := parseSpecPaths(tc.paths)
if ok, msg := checker.DeepEqual(got, tc.expected); !ok {
t.Errorf("case %q failed:\n%s", tc.name, msg)
}
})
}
}

func TestAllowedFlagsToDeniedPaths(t *testing.T) {
sampleFlags := PathSet{
"GetEndpoint": {Method: "GET", Path: "/endpoint"},
"PutEndpointID": {Method: "PUT", Path: "/endpoint/{id}"},
"PatchEndpointIDConfig": {Method: "PATCH", Path: "/endpoint/{id}/config"},
}
testCases := [...]struct {
name string
allowed []string
expected PathSet
expectedErr error
}{
{
name: "deny all",
allowed: []string{},
expected: PathSet{
"GetEndpoint": {Method: "GET", Path: "/endpoint"},
"PutEndpointID": {Method: "PUT", Path: "/endpoint/{id}"},
"PatchEndpointIDConfig": {Method: "PATCH", Path: "/endpoint/{id}/config"},
},
},
{
name: "wildcard: allow all",
allowed: []string{"*"},
expected: PathSet{},
},
{
name: "wildcard: allow gets",
allowed: []string{"Get*"},
expected: PathSet{
"PutEndpointID": {Method: "PUT", Path: "/endpoint/{id}"},
"PatchEndpointIDConfig": {Method: "PATCH", Path: "/endpoint/{id}/config"},
},
},
{
name: "allow invalid option",
allowed: []string{"NoSuchOption"},
expected: PathSet(nil),
expectedErr: ErrUnknownFlag,
},
{
name: "deny all empty string",
allowed: []string{""},
expected: PathSet(nil),
expectedErr: ErrUnknownFlag,
},
{
name: "wildcard: invalid prefix",
allowed: []string{"*foo"},
expected: PathSet(nil),
expectedErr: ErrUnknownWildcard,
},
{
name: "wildcard: invalid multiple wildcard",
allowed: []string{"foo*bar*"},
expected: PathSet(nil),
expectedErr: ErrUnknownWildcard,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := generateDeniedAPIEndpoints(sampleFlags, tc.allowed)
if ok, msg := checker.DeepEqual(got, tc.expected); !ok {
t.Errorf("case %q failed:\n%s", tc.name, msg)
}
if ok, msg := checker.DeepEqual(errors.Unwrap(err), tc.expectedErr); !ok {
t.Errorf("case %q error mismatch:\n%s", tc.name, msg)
}
})
}

}

0 comments on commit e8440eb

Please sign in to comment.