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

Merge branch '7-rego-functions-for-did-to-url-tranformation' into 'main'

Rego extension functions to transform DID:WEB to URL and vice-versa

Closes #7

See merge request gaia-x/data-infrastructure-federation-services/tsa/policy!5
parents e451b697 bf2d98d8
No related branches found
No related tags found
No related merge requests found
Pipeline #54465 passed with stages
in 2 minutes and 39 seconds
......@@ -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.NewDIDWebFuncs()
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.DIDToURLFunc()))
regofunc.Register("urlToDID", rego.Function1(didTransformerFuncs.URLToDIDFunc()))
}
// 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 DIDWebFuncs 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 NewDIDWebFuncs() *DIDWebFuncs {
return &DIDWebFuncs{}
}
func (dw *DIDWebFuncs) DIDToURLFunc() (*rego.Function, rego.Builtin1) {
return &rego.Function{
Name: "did_to_url",
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 := dw.didToURL(did)
if err != nil {
return nil, err
}
return ast.StringTerm(u.String()), nil
}
}
func (dw *DIDWebFuncs) URLToDIDFunc() (*rego.Function, rego.Builtin1) {
return &rego.Function{
Name: "url_to_did",
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 := dw.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 (dw *DIDWebFuncs) 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 (dw *DIDWebFuncs) urlToDID(uri *url.URL) *DID {
p := strings.TrimSuffix(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 TestDIDToURLFunc(t *testing.T) {
tests := []struct {
// test input
name string
regoQuery string
// expected result
res string
errText string
}{
{
name: "DID is empty",
regoQuery: `did_to_url("")`,
errText: "DID cannot be empty",
},
{
name: "invalid DID",
regoQuery: `did_to_url("invalid-did")`,
errText: "invalid DID, host is not found",
},
{
name: "invalid DID Method",
regoQuery: `did_to_url("did:sov:123456qwerty")`,
errText: "invalid DID, method is unknown",
},
{
name: "transformation success with DID containing domain only",
regoQuery: `did_to_url("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: `did_to_url("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: `did_to_url("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.NewDIDWebFuncs()
r := rego.New(
rego.Query(test.regoQuery),
rego.Function1(DIDTransformerFuncs.DIDToURLFunc()),
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 TestURLToDIDFunc(t *testing.T) {
tests := []struct {
// test input
name string
regoQuery string
// expected result
res string
errText string
}{
{
name: "empty URL",
regoQuery: `url_to_did("")`,
errText: "URL cannot be empty",
},
{
name: "URL containing special characters",
regoQuery: `url_to_did("example.com\nH1234")`,
errText: "cannot parse URL",
},
{
name: "URL does not contain secure protocol (https)",
regoQuery: `url_to_did("example.com")`,
errText: "invalid URL for did:web method",
},
{
name: "URL does not contain valid domain",
regoQuery: `url_to_did("https://")`,
errText: "invalid URL for did:web method",
},
{
name: "transformation success with URL containing domain only",
regoQuery: `url_to_did("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: `url_to_did("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: `url_to_did("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.NewDIDWebFuncs()
r := rego.New(
rego.Query(test.regoQuery),
rego.Function1(DIDTransformerFuncs.URLToDIDFunc()),
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