Skip to content

Commit

Permalink
Merge pull request #1 from cofide/opinionated-ids
Browse files Browse the repository at this point in the history
Add library for opinionated SPIFFE IDs
  • Loading branch information
markgoddard authored Jan 21, 2025
2 parents a4125a5 + 099432b commit 3217cb2
Show file tree
Hide file tree
Showing 8 changed files with 604 additions and 7 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]

jobs:
lint:
Expand All @@ -17,13 +16,13 @@ jobs:
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install golangci-lint
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
args: --timeout=5m

build-test:
name: build
name: test
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -37,5 +36,5 @@ jobs:
- name: Install dependencies
run: |
go mod download
- name: Build and run tests
run: just test
- name: Build and run tests with race detector enabled
run: just test-race
6 changes: 4 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
test:
go run gotest.tools/gotestsum@latest --format github-actions ./...
test *args:
go run gotest.tools/gotestsum@latest --format github-actions ./... {{args}}

test-race: (test "--" "-race")

lint *args:
golangci-lint run --show-stats {{args}}
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/cofide/cofide-sdk-go

go 1.23.3

require (
github.com/gobwas/glob v0.2.3
github.com/spiffe/go-spiffe/v2 v2.4.0
)

require github.com/zeebo/errs v1.3.0 // indirect
32 changes: 32 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spiffe/go-spiffe/v2 v2.4.0 h1:j/FynG7hi2azrBG5cvjRcnQ4sux/VNj8FAVc99Fl66c=
github.com/spiffe/go-spiffe/v2 v2.4.0/go.mod h1:m5qJ1hGzjxjtrkGHZupoXHo/FDWwCB1MdSyBzfHugx0=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
149 changes: 149 additions & 0 deletions pkg/id/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright 2024 Cofide Limited.
// SPDX-License-Identifier: Apache-2.0

package id

import (
"crypto/x509"
"errors"
"fmt"

"github.com/gobwas/glob"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
)

// MatchFunc is a function that can be called to determine whether the path
// component of a SPIFFEID matches a given constraint. The function should
// return nil if the constraint matches, or an error otherwise.
type MatchFunc func(kv map[string]string) error

// Matches applies a set of MatchFunc functions to a SPIFFEID and returns the
// combined match result. If no MatchFunc returns an error, then Match returns
// nil. Otherwise an error is returned.
func (s *SPIFFEID) Matches(funcs ...MatchFunc) error {
kv, err := s.ParsePath()
if err != nil {
return err
}

for _, f := range funcs {
err := f(kv)
if err != nil {
return err
}
}

return nil
}

// AuthorizeMatch returns a [tlsconfig.Authorizer] that authorizes an ID when
// it matches all of the provided MatchFunc.
func AuthorizeMatch(funcs ...MatchFunc) tlsconfig.Authorizer {
return func(id spiffeid.ID, verifiedChains [][]*x509.Certificate) error {
sid, err := ParseID(id.String())
if err != nil {
return err
}

err = sid.Matches(funcs...)
if err != nil {
return err
}
return nil
}
}

// Equals returns a MatchFunc that matches any ID that contains the specified
// key/value pair.
func Equals(key, value string) MatchFunc {
return func(kv map[string]string) error {
if val, ok := kv[key]; !ok || val != value {
return fmt.Errorf("key %s does not match value %s", key, value)
}

return nil
}
}

// IsEmptyKey returns a MatchFunc that matches any ID that contains the
// specified key with an empty value.
func IsEmpty(key string) MatchFunc {
return func(kv map[string]string) error {
if kv[key] != "" {
return fmt.Errorf("key %s is not empty", key)
}

return nil
}
}

// IsNotEmpty returns a MatchFunc that matches any ID that contains the
// specified key with a non-empty value.
func IsNotEmpty(key string) MatchFunc {
return func(kv map[string]string) error {
if kv[key] == "" {
return fmt.Errorf("key %s is empty", key)
}

return nil
}
}

// MatchGlob returns a MatchFunc that matches any ID that contains the
// specified key with a value matching the specified glob pattern.
func MatchGlob(key, globStr string) MatchFunc {
return func(kv map[string]string) error {
g, err := glob.Compile(globStr)
if err != nil {
return fmt.Errorf("failed to compile glob %q: %w", globStr, err)
}
if _, ok := kv[key]; !ok {
return fmt.Errorf("key %q not found", key)
}
if !g.Match(kv[key]) {
return fmt.Errorf("key %q with value %q does not match glob %q", key, kv[key], globStr)
}

return nil
}
}

// Or returns a MatchFunc that combines the specified MatchFunc using a logical
// OR.
func Or(funcs ...MatchFunc) MatchFunc {
return func(kv map[string]string) error {
errs := make([]error, 0, len(funcs))
for _, f := range funcs {
err := f(kv)
errs = append(errs, err)
}

hasPassedTest := false
for _, err := range errs {
if err == nil {
hasPassedTest = true
break
}
}

if !hasPassedTest {
return fmt.Errorf("none of the tests passed")
}

return nil
}
}

// Not returns a MatchFunc that logically inverts the result of the specified
// MatchFunc.
func Not(f MatchFunc) MatchFunc {
return func(kv map[string]string) error {
err := f(kv)
if err == nil {
return errors.New("Did not receive an error in NOT call")
}

return nil
}
}
145 changes: 145 additions & 0 deletions pkg/id/query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright 2024 Cofide Limited.
// SPDX-License-Identifier: Apache-2.0

package id

import (
"testing"
)

func TestSPIFFEID_Matches(t *testing.T) {
type args struct {
funcs []MatchFunc
}
tests := []struct {
name string
id *SPIFFEID
args args
wantErr bool
}{
{
name: "Simple KV match",
id: MustParseID("spiffe://example.org/key1/value1/key2/value2"),
args: args{
funcs: []MatchFunc{
Equals("key1", "value1"),
Equals("key2", "value2"),
},
},
wantErr: false,
},
{
name: "Simple KV mismatch",
id: MustParseID("spiffe://example.org/key1/value1/key2/value2"),
args: args{
funcs: []MatchFunc{
Equals("key1", "value1"),
Equals("key2", "value3"),
},
},
wantErr: true,
},
{
name: "Simple OR",
id: MustParseID("spiffe://example.org/ns/kube-system/sa/default/deploy/coredns"),
args: args{
funcs: []MatchFunc{
Equals("ns", "kube-system"),
Or(Equals("deploy", "kube-dns"), Equals("deploy", "coredns")),
},
},
wantErr: false,
},
{
name: "Simple OR mismatch",
id: MustParseID("spiffe://example.org/ns/kube-system/sa/default/deploy/coredns"),
args: args{
funcs: []MatchFunc{
Equals("ns", "kube-system"),
Or(Equals("deploy", "kube-dns"), Equals("deploy", "kube-proxy")),
},
},
wantErr: true,
},
{
name: "Simple Glob",
id: MustParseID("spiffe://example.org/ns/kube-system/sa/default/deploy/coredns"),
args: args{
funcs: []MatchFunc{
MatchGlob("deploy", "core*"),
},
},
wantErr: false,
},
{
name: "Simple Glob mismatch",
id: MustParseID("spiffe://example.org/ns/kube-system/sa/default/deploy/coredns"),
args: args{
funcs: []MatchFunc{
MatchGlob("deploy", "kube*"),
},
},
wantErr: true,
},
{
name: "Simple isEmpty",
id: MustParseID("spiffe://example.org/ns/kube-system/sa/default/deploy/coredns"),
args: args{
funcs: []MatchFunc{
IsEmpty("cluster"),
},
},
wantErr: false,
},
{
name: "Simple isEmpty mismatch",
id: MustParseID("spiffe://example.org/ns/kube-system/sa/default/deploy/coredns"),
args: args{
funcs: []MatchFunc{
IsEmpty("ns"),
},
},
wantErr: true,
},
{
name: "Simple isNotEmpty",
id: MustParseID("spiffe://example.org/ns/kube-system/sa/default/deploy/coredns"),
args: args{
funcs: []MatchFunc{
IsNotEmpty("ns"),
},
},
wantErr: false,
},
{
name: "Simple isNotEmpty mismatch",
id: MustParseID("spiffe://example.org/ns/kube-system/sa/default/deploy/coredns"),
args: args{
funcs: []MatchFunc{
IsNotEmpty("cluster"),
},
},
wantErr: true,
},
{
name: "Simple Not",
id: MustParseID("spiffe://example.org/ns/kube-system/sa/default/deploy/coredns"),
args: args{
funcs: []MatchFunc{
Not(IsEmpty("ns")),
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := tt.id
err := s.Matches(tt.args.funcs...)
if (err != nil) != tt.wantErr {
t.Errorf("SPIFFEID.Matches() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
Loading

0 comments on commit 3217cb2

Please sign in to comment.