Skip to content
Snippets Groups Projects
Commit 6c6ab4f0 authored by Yordan Kinkov's avatar Yordan Kinkov
Browse files

Add extension functions for DID to URL transformation

parent e451b697
No related branches found
No related tags found
No related merge requests found
......@@ -87,6 +87,7 @@ func main() {
taskFuncs := regofunc.NewTaskFuncs(cfg.Task.Addr, httpClient)
ocmFuncs := regofunc.NewOcmFuncs(cfg.OCM.Addr, httpClient)
signerFuncs := regofunc.NewSignerFuncs(cfg.Signer.Addr, httpClient)
didTransformerFuncs := regofunc.NewDIDTransformerFuncs()
regofunc.Register("cacheGet", rego.Function3(cacheFuncs.CacheGetFunc()))
regofunc.Register("cacheSet", rego.Function4(cacheFuncs.CacheSetFunc()))
regofunc.Register("didResolve", rego.Function1(didResolverFuncs.ResolveFunc()))
......@@ -99,6 +100,8 @@ func main() {
regofunc.Register("verifyProof", rego.Function1(signerFuncs.VerifyProof()))
regofunc.Register("ocmLoginProofInvitation", rego.Function2(ocmFuncs.GetLoginProofInvitation()))
regofunc.Register("ocmLoginProofResult", rego.Function1(ocmFuncs.GetLoginProofResult()))
regofunc.Register("didToURL", rego.Function1(didTransformerFuncs.ToURLFunc()))
regofunc.Register("urlToDID", rego.Function1(didTransformerFuncs.FromURLFunc()))
}
// subscribe the cache for policy data changes
......
package regofunc
import (
"fmt"
"net/url"
"strings"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/types"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/golib/errors"
)
const (
didSeparator = ":"
urlSeparator = "/"
defaultUrlPath = ".well-known"
)
type DIDTransformerFuncs struct{}
type DID struct {
scheme string // scheme is always "did"
method string // method is the specific did method - "web" in this case
path string // path is the unique URI assigned by the DID method
}
func NewDIDTransformerFuncs() *DIDTransformerFuncs {
return &DIDTransformerFuncs{}
}
func (dt *DIDTransformerFuncs) ToURLFunc() (*rego.Function, rego.Builtin1) {
return &rego.Function{
Name: "url_from_did",
Decl: types.NewFunction(types.Args(types.S), types.A),
Memoize: true,
},
func(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
var did string
if err := ast.As(a.Value, &did); err != nil {
return nil, fmt.Errorf("invalid DID: %s", err)
}
if did == "" {
return nil, errors.New("DID cannot be empty")
}
u, err := dt.didToURL(did)
if err != nil {
return nil, err
}
return ast.StringTerm(u.String()), nil
}
}
func (dt *DIDTransformerFuncs) FromURLFunc() (*rego.Function, rego.Builtin1) {
return &rego.Function{
Name: "did_from_url",
Decl: types.NewFunction(types.Args(types.S), types.A),
Memoize: true,
},
func(bctx rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
var u string
if err := ast.As(a.Value, &u); err != nil {
return nil, fmt.Errorf("invalid URL: %s", err)
}
if u == "" {
return nil, errors.New("URL cannot be empty")
}
uri, err := url.Parse(u)
if err != nil {
return nil, errors.New("cannot parse URL")
}
if uri.Host == "" || uri.Scheme != "https" {
return nil, errors.New("invalid URL for did:web method")
}
did := dt.urlToDID(uri)
return ast.StringTerm(did.String()), nil
}
}
// didToURL transforms a valid DID, created by the "did:web" Method Specification, to a URL.
// Documentation can be found here: https://w3c-ccg.github.io/did-method-web/
func (dt *DIDTransformerFuncs) didToURL(DID string) (*url.URL, error) {
ss := strings.Split(DID, didSeparator)
if len(ss) < 3 {
return nil, errors.New("invalid DID, host is not found")
}
if ss[0] != "did" || ss[1] != "web" {
return nil, errors.New("invalid DID, method is unknown")
}
path := defaultUrlPath
if len(ss) > 3 {
path = ""
for i := 3; i < len(ss); i++ {
path = path + urlSeparator + ss[i]
}
}
path = path + urlSeparator + "did.json"
host, err := url.PathUnescape(ss[2])
if err != nil {
return nil, errors.New("failed to url decode host from DID")
}
return &url.URL{
Scheme: "https",
Host: host,
Path: path,
}, nil
}
// urlToDID transforms a valid URL to a DID created following the "did:web" Method Specification.
// Documentation can be found here: https://w3c-ccg.github.io/did-method-web/
func (dt *DIDTransformerFuncs) urlToDID(uri *url.URL) *DID {
p := strings.TrimRight(uri.Path, "did.json")
sp := strings.Split(p, urlSeparator)
path := url.QueryEscape(uri.Host)
for _, v := range sp {
if v == defaultUrlPath {
break
}
if v == "" {
continue
}
path = path + didSeparator + url.QueryEscape(v)
}
return &DID{
scheme: "did",
method: "web",
path: strings.Trim(path, didSeparator),
}
}
// String returns a string representation of this DID.
func (d *DID) String() string {
return fmt.Sprintf("%s:%s:%s", d.scheme, d.method, d.path)
}
package regofunc_test
import (
"context"
"encoding/json"
"testing"
"github.com/open-policy-agent/opa/rego"
"github.com/stretchr/testify/assert"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/regofunc"
)
func TestToURLFunc(t *testing.T) {
tests := []struct {
// test input
name string
regoQuery string
// expected result
res string
errText string
}{
{
name: "DID is empty",
regoQuery: `url_from_did("")`,
errText: "DID cannot be empty",
},
{
name: "invalid DID",
regoQuery: `url_from_did("invalid-did")`,
errText: "invalid DID, host is not found",
},
{
name: "invalid DID Method",
regoQuery: `url_from_did("did:sov:123456qwerty")`,
errText: "invalid DID, method is unknown",
},
{
name: "transformation success with DID containing domain only",
regoQuery: `url_from_did("did:web:w3c-ccg.github.io")`,
res: "\"https://w3c-ccg.github.io/.well-known/did.json\"",
},
{
name: "transformation success with DID containing domain and path",
regoQuery: `url_from_did("did:web:w3c-ccg.github.io:user:alice")`,
res: "\"https://w3c-ccg.github.io/user/alice/did.json\"",
},
{
name: "transformation success with DID containing network port",
regoQuery: `url_from_did("did:web:example.com%3A3000:user:alice")`,
res: "\"https://example.com:3000/user/alice/did.json\"",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
DIDTransformerFuncs := regofunc.NewDIDTransformerFuncs()
r := rego.New(
rego.Query(test.regoQuery),
rego.Function1(DIDTransformerFuncs.ToURLFunc()),
rego.StrictBuiltinErrors(true),
)
resultSet, err := r.Eval(context.Background())
if err == nil {
resultBytes, err := json.Marshal(resultSet[0].Expressions[0].Value)
assert.NoError(t, err)
assert.Equal(t, test.res, string(resultBytes))
} else {
assert.ErrorContains(t, err, test.errText)
}
})
}
}
func TestFromURLFunc(t *testing.T) {
tests := []struct {
// test input
name string
regoQuery string
// expected result
res string
errText string
}{
{
name: "empty URL",
regoQuery: `did_from_url("")`,
errText: "URL cannot be empty",
},
{
name: "URL containing special characters",
regoQuery: `did_from_url("example.com\nH1234")`,
errText: "cannot parse URL",
},
{
name: "URL does not contain secure protocol (https)",
regoQuery: `did_from_url("example.com")`,
errText: "invalid URL for did:web method",
},
{
name: "URL does not contain valid domain",
regoQuery: `did_from_url("https://")`,
errText: "invalid URL for did:web method",
},
{
name: "transformation success with URL containing domain only",
regoQuery: `did_from_url("https://w3c-ccg.github.io/.well-known/did.json")`,
res: "\"did:web:w3c-ccg.github.io\"",
},
{
name: "transformation success with URL containing domain with path",
regoQuery: `did_from_url("https://w3c-ccg.github.io/user/alice/did.json")`,
res: "\"did:web:w3c-ccg.github.io:user:alice\"",
},
{
name: "transformation success with URL containing network port",
regoQuery: `did_from_url("https://example.com:3000/user/alice/did.json")`,
res: "\"did:web:example.com%3A3000:user:alice\"",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
DIDTransformerFuncs := regofunc.NewDIDTransformerFuncs()
r := rego.New(
rego.Query(test.regoQuery),
rego.Function1(DIDTransformerFuncs.FromURLFunc()),
rego.StrictBuiltinErrors(true),
)
resultSet, err := r.Eval(context.Background())
if err == nil {
resultBytes, err := json.Marshal(resultSet[0].Expressions[0].Value)
assert.NoError(t, err)
assert.Equal(t, test.res, string(resultBytes))
} else {
assert.ErrorContains(t, err, test.errText)
}
})
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment