-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
api: Add libraries to Pascalify API endpoints
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
1 parent
8544406
commit e8440eb
Showing
2 changed files
with
328 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
|
||
} |