Skip to content

Commit 45bb71e

Browse files
authored
Adds ability to configure certificate-filters on a namespace via the CLI (#43)
Adds new namespace sub-command called certificate-filters ("cf"). This sub-commands has 3 sub-commands: import/imp --> imports certificate filter configuration from either a file or from input on the CLI export/exp --> takes the existing certificate filter configuration and writes it to the output (and also optionally to a file) clear --> removes all certificate filters on the namespace. import/clear will print out the before/after of the certificate-filter configuration to make it more obvious to the user what is being changed.
1 parent b4e39e7 commit 45bb71e

6 files changed

Lines changed: 1413 additions & 306 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ TEST_ARG ?= -race -timeout=5m -cover -count=1
1919
tcld:
2020
@go build -ldflags "$(LINKER_FLAGS)" -o tcld ./cmd/tcld/*.go
2121

22-
bins: tcld
22+
bins: clean tcld
2323

2424
test:
2525
@$(foreach TEST_DIR,$(TEST_DIRS),\

api/temporalcloudapi/namespace/v1/message.pb.go

Lines changed: 533 additions & 54 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/cafilters.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package app
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/temporalio/tcld/api/temporalcloudapi/namespace/v1"
10+
)
11+
12+
type certificateFilter struct {
13+
CommonName string `json:"commonName"`
14+
Organization string `json:"organization"`
15+
OrganizationalUnit string `json:"organizationalUnit"`
16+
SubjectAlternativeName string `json:"subjectAlternativeName"`
17+
}
18+
19+
type certificateFiltersConfig struct {
20+
Filters []certificateFilter `json:"filters,omitempty"`
21+
}
22+
23+
func parseCertificateFilters(configJson []byte) (certificateFiltersConfig, error) {
24+
if len(configJson) == 0 {
25+
return certificateFiltersConfig{}, nil
26+
}
27+
28+
var filters certificateFiltersConfig
29+
if err := json.Unmarshal(configJson, &filters); err != nil {
30+
return certificateFiltersConfig{}, err
31+
}
32+
33+
if err := filters.validate(); err != nil {
34+
return certificateFiltersConfig{}, err
35+
}
36+
37+
return filters, nil
38+
}
39+
40+
func (config certificateFiltersConfig) validate() error {
41+
seenSet := make(map[certificateFilter]struct{})
42+
43+
for _, filter := range config.Filters {
44+
if !isFieldSet(filter.CommonName) && !isFieldSet(filter.Organization) && !isFieldSet(filter.OrganizationalUnit) && !isFieldSet(filter.SubjectAlternativeName) {
45+
return errors.New("certificate filter must have at least one field set")
46+
}
47+
48+
if _, ok := seenSet[filter]; ok {
49+
return fmt.Errorf("supplied certificate filters contain at least one duplicate entry: '%+v'", filter)
50+
}
51+
52+
seenSet[filter] = struct{}{}
53+
}
54+
55+
return nil
56+
}
57+
58+
func (config certificateFiltersConfig) toSpec() []*namespace.CertificateFilterSpec {
59+
var results []*namespace.CertificateFilterSpec
60+
61+
for _, filter := range config.Filters {
62+
results = append(results, &namespace.CertificateFilterSpec{
63+
CommonName: filter.CommonName,
64+
Organization: filter.Organization,
65+
OrganizationalUnit: filter.OrganizationalUnit,
66+
SubjectAlternativeName: filter.SubjectAlternativeName,
67+
})
68+
}
69+
70+
return results
71+
}
72+
73+
func fromSpec(filters []*namespace.CertificateFilterSpec) certificateFiltersConfig {
74+
var result certificateFiltersConfig
75+
76+
for _, filter := range filters {
77+
result.Filters = append(result.Filters, certificateFilter{
78+
CommonName: filter.CommonName,
79+
Organization: filter.Organization,
80+
OrganizationalUnit: filter.OrganizationalUnit,
81+
SubjectAlternativeName: filter.SubjectAlternativeName,
82+
})
83+
}
84+
85+
return result
86+
}
87+
88+
func isFieldSet(fieldValue string) bool {
89+
return len(strings.TrimSpace(fieldValue)) > 0
90+
}

app/cafilters_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package app
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func Test_certificateFilters_validate(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
filters certificateFiltersConfig
12+
wantErr bool
13+
}{
14+
{
15+
"no filters specified",
16+
certificateFiltersConfig{},
17+
false,
18+
},
19+
{
20+
"empty filter specified",
21+
certificateFiltersConfig{
22+
[]certificateFilter{{}},
23+
},
24+
true,
25+
},
26+
{
27+
"duplicate filter specified",
28+
certificateFiltersConfig{
29+
Filters: []certificateFilter{
30+
{CommonName: "testCN"},
31+
{CommonName: "testCN"},
32+
},
33+
},
34+
true,
35+
},
36+
{
37+
"two separate filters specified",
38+
certificateFiltersConfig{
39+
Filters: []certificateFilter{
40+
{CommonName: "testCN"},
41+
{CommonName: "testCN2"},
42+
},
43+
},
44+
false,
45+
},
46+
}
47+
48+
for _, tt := range tests {
49+
t.Run(tt.name, func(t *testing.T) {
50+
if err := tt.filters.validate(); (err != nil) != tt.wantErr {
51+
t.Errorf("certificateFilters.validate() error = %v, wantErr %v", err, tt.wantErr)
52+
}
53+
})
54+
}
55+
}
56+
57+
func Test_parseCertificateFilters(t *testing.T) {
58+
type args struct {
59+
jsonFilters string
60+
}
61+
tests := []struct {
62+
name string
63+
args args
64+
want certificateFiltersConfig
65+
wantErr bool
66+
}{
67+
{
68+
"empty contents",
69+
args{},
70+
certificateFiltersConfig{},
71+
false,
72+
},
73+
{
74+
"empty array",
75+
args{`{ "filters": [] }`},
76+
certificateFiltersConfig{
77+
Filters: []certificateFilter{},
78+
},
79+
false,
80+
},
81+
{
82+
"bad json",
83+
args{`[`},
84+
certificateFiltersConfig{},
85+
true,
86+
},
87+
{
88+
"simple json",
89+
args{`{"filters": [{"commonName": "test1"}]}`},
90+
certificateFiltersConfig{
91+
Filters: []certificateFilter{{CommonName: "test1"}},
92+
},
93+
false,
94+
},
95+
{
96+
"complex json",
97+
args{`{ "filters": [ { "commonName": "test1" }, { "commonName": "test2" } ] }`},
98+
certificateFiltersConfig{
99+
Filters: []certificateFilter{
100+
{CommonName: "test1"},
101+
{CommonName: "test2"},
102+
},
103+
},
104+
false,
105+
},
106+
{
107+
"fail validation",
108+
args{`{"filters": [{}]}`},
109+
certificateFiltersConfig{},
110+
true,
111+
},
112+
}
113+
114+
for _, tt := range tests {
115+
t.Run(tt.name, func(t *testing.T) {
116+
got, err := parseCertificateFilters([]byte(tt.args.jsonFilters))
117+
if (err != nil) != tt.wantErr {
118+
t.Errorf("parseCertificateFilters() error = %v, wantErr %v", err, tt.wantErr)
119+
return
120+
}
121+
if !reflect.DeepEqual(got, tt.want) {
122+
t.Errorf("parseCertificateFilters() = %v, want %v", got, tt.want)
123+
}
124+
})
125+
}
126+
}

0 commit comments

Comments
 (0)