diff --git a/.travis.yml b/.travis.yml index 3ce4fb3..3d5e428 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,9 @@ services: install: true script: - - go build ./... + - go install ./... - go test ./... + - pass-policy-service validate policies/*.json - docker-compose build policyservice - docker-compose pull fcrepo && docker-compose up -d && bash ./scripts/wait_for_docker.sh - go test -v -tags=integration ./... || docker logs policyservice diff --git a/cmd/pass-policy-service/validate.go b/cmd/pass-policy-service/validate.go index cd8257f..e7c3fa8 100644 --- a/cmd/pass-policy-service/validate.go +++ b/cmd/pass-policy-service/validate.go @@ -4,9 +4,9 @@ import ( "fmt" "io/ioutil" "log" + "strings" "github.com/oa-pass/pass-policy-service/rule" - "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -14,15 +14,16 @@ func validate() cli.Command { return cli.Command{ Name: "validate", - Usage: "Validate a given policy rules file", + Usage: "Validate policy rules files", Description: ` - Given a policy rules file, validate will attempt to parse the document - and validate it with respect to the schema used by this polivy service. + Given a list of policy rules files, validate will attempt to parse + the documents and validate it with respect to the schema used by this + policy service. - Note, the document will be validated against schemas supported by this + Note, the documents will be validated against schemas supported by this application regardless of any schema declarations in the file. `, - ArgsUsage: "file", + ArgsUsage: "files", Action: func(c *cli.Context) error { return validateAction(c.Args()) }, @@ -30,19 +31,28 @@ func validate() cli.Command { } func validateAction(args []string) error { - if len(args) != 1 { - return fmt.Errorf("validate expects exactly one argument") + if len(args) < 1 { + return fmt.Errorf("validate requires at least one schema") } - content, err := ioutil.ReadFile(args[0]) - if err != nil { - return errors.Wrapf(err, "error opening file") + var lastErr error + + for _, instance := range args { + content, err := ioutil.ReadFile(instance) + if err != nil { + lastErr = err + continue + } + + _, err = rule.Validate(content) + if err == nil { + log.Printf("Validation OK: %s", instance) + } else { + errtxt := strings.ReplaceAll(fmt.Sprintf("%v", err), "\n", "\n ") + log.Printf("Validation failed: %s:\n %v", instance, errtxt) + lastErr = err + } } - _, err = rule.Validate(content) - if err == nil { - log.Println("Validation OK") - } - - return err + return lastErr } diff --git a/policies/harvard.json b/policies/harvard.json new file mode 100644 index 0000000..97fd95f --- /dev/null +++ b/policies/harvard.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://oa-pass.github.io/pass-policy-service/schemas/policy_config_1.0.json", + "policy-rules": [ + { + "description": "Must deposit to one of the repositories indicated by primary funder", + "policy-id": "${submission.grants.primaryFunder.policy}", + "type": "funder", + "repositories": [ + { + "repository-id": "${policy.repositories}" + } + ] + }, + { + "description": "Must deposit to one of the repositories indicated by direct funder", + "policy-id": "${submission.grants.directFunder.policy}", + "type": "funder", + "repositories": [ + { + "repository-id": "${policy.repositories}" + } + ] + }, + { + "description": "Faculty members must deposit into DASH", + "policy-id": "/policies/f9/b6/01/25/f9b60125-662d-4e03-a7b0-eec0df2b50de", + "type": "institution", + "conditions": [ + { + "endsWith": { + "@harvard.edu": "${header.Ajp_eppn}" + } + }, + { + "contains": { + "FACULTY": "${header.Ajp_affiliation}" + } + } + ], + "repositories": [ + { + "repository-id": "/repositories/93/c5/ff/37/93c5ff37-ca2b-4652-a2af-3b6794ff8790" + } + ] + }, + { + "description": "Non-faculty members may optionally deposit into DASH", + "policy-id": "/policies/f9/b6/01/25/f9b60125-662d-4e03-a7b0-eec0df2b50de", + "type": "institution", + "conditions": [ + { + "endsWith": { + "@harvard.edu": "${header.Ajp_eppn}" + } + }, + { + "noneOf": [ + { + "contains": { + "FACULTY": "${header.Ajp_Affiliation}" + } + } + ] + } + ], + "repositories": [ + { + "repository-id": "/repositories/93/c5/ff/37/93c5ff37-ca2b-4652-a2af-3b6794ff8790", + "selected": true + }, + { + "repository-id": "*" + } + ] + } + ] +} \ No newline at end of file diff --git a/rule/README.md b/rule/README.md index 24608cc..327e0d1 100644 --- a/rule/README.md +++ b/rule/README.md @@ -72,7 +72,12 @@ Policy inclusion rules are JSON objects containing the following fields: * `policy-id`: a string containing a a single policy URI, or a variable substitution resulting in one or more policy URIs * In the case of a variable substitution resulting in many URIs, it is equivalent to creating multiple policy rules, each one containing a single policy-id from that list. repositories: contains a list of repository description JSON objects, specifying which repositories satisfy the given policy. -* `condition`: Optional. JSON object describing a condition where the policy is included only if the condition evaluates to true. If this field is not present, it is presumed that inclusion of the policy is unconditional +* `condition`: Optional. JSON object describing a condition where the policy is included only if the condition evaluates to true. If this field is not present, it is presumed that inclusion of the policy is unconditional. See the schema for more details, but conditions include: + * `equals`: true if two strings are equal + * `endsWith`: true if a string ends with another + * `contains`: true if a string contains another as a substring + * `anyOf`: true if any of the given list of conditions are true + * `noneOf`: true if none of the given list of conditions are true Repositories are JSON objects with the following fields: diff --git a/rule/condition.go b/rule/condition.go index 4de77f7..25fa90d 100644 --- a/rule/condition.go +++ b/rule/condition.go @@ -25,6 +25,8 @@ func init() { "endsWith": endsWith, "equals": equals, "anyOf": anyOf, + "noneOf": noneOf, + "contains": contains, } } @@ -54,6 +56,11 @@ func endsWith(fromCondition interface{}, variables VariableResolver) (bool, erro return eachPair(fromCondition, variables, strings.HasSuffix) } +func contains(fromCondition interface{}, variables VariableResolver) (bool, error) { + + return eachPair(fromCondition, variables, strings.Contains) +} + func equals(fromCondition interface{}, variables VariableResolver) (bool, error) { return eachPair(fromCondition, variables, func(a, b string) bool { @@ -86,6 +93,11 @@ func anyOf(arg interface{}, variables VariableResolver) (bool, error) { return false, nil } +func noneOf(arg interface{}, variables VariableResolver) (bool, error) { + passes, err := anyOf(arg, variables) + return !passes, err +} + func eachPair(src interface{}, variables VariableResolver, test func(string, string) bool) (passes bool, err error) { operands, ok := src.(map[string]interface{}) if !ok { diff --git a/rule/condition_test.go b/rule/condition_test.go index 82929a3..41802cc 100644 --- a/rule/condition_test.go +++ b/rule/condition_test.go @@ -32,12 +32,34 @@ func TestConditionApply(t *testing.T) { {"endsWith":{"one": "goner"}} ] }`, + }, { + expected: true, + json: `{ + "noneOf": [ + {"equals":{"one": "two"}}, + {"endsWith":{"one": "goner"}} + ] + }`, + }, { + expected: false, + json: `{ + "noneOf": [ + {"equals":{"one": "two"}}, + {"endsWith":{"one": "gone"}} + ] + }`, }, { expected: false, json: `{"equals":{"one": "two"}}`, }, { expected: true, json: `{"equals":{"two": "two"}}`, + }, { + expected: true, + json: `{"contains": {"FACULTY": "STAFF,FACULTY,COW"}}`, + }, { + expected: false, + json: `{"contains": {"BOVINE": "STAFF,FACULTY,COW"}}`, }, { expected: true, json: `{ diff --git a/rule/testdata/good.json b/rule/testdata/good.json index 65bc64c..73f1ede 100644 --- a/rule/testdata/good.json +++ b/rule/testdata/good.json @@ -30,6 +30,15 @@ "endsWith": { "@johnshopkins.edu": "${header.Eppn}" } + }, + { + "noneOf": [ + { + "contains": { + "foo": "${header.Foo}" + } + } + ] } ], "repositories": [ diff --git a/schemas/policy_config_1.0.json b/schemas/policy_config_1.0.json index 8186a73..9b7a46f 100644 --- a/schemas/policy_config_1.0.json +++ b/schemas/policy_config_1.0.json @@ -79,6 +79,9 @@ }, { "$ref": "#/definitions/anyOf" + }, + { + "$ref": "#/definitions/noneOf" } ] } @@ -127,6 +130,25 @@ } } } + }, + { + "type": "object", + "required": [ + "contains" + ], + "additionalProperties": false, + "properties": { + "contains": { + "type": "object", + "title": "Contains", + "description": "Evaluates to 'true' when the given value contains the key", + "patternProperties": { + "^.+$": { + "type": "string" + } + } + } + } } ] }, @@ -144,6 +166,21 @@ } } } + }, + "noneOf": { + "type": "object", + "title": "None Of", + "description": "Evaluates to true when none the conditions listed within evaluate to true", + "required": ["noneOf"], + "additionalProperties": false, + "properties": { + "noneOf": { + "type": "array", + "items": { + "$ref": "#/definitions/condition" + } + } + } } } } \ No newline at end of file