lib/validator/claim.go (159 lines of code) (raw):
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
"context"
"fmt"
"reflect"
"regexp"
"time"
"bitbucket.org/creachadair/stringset" /* copybara-comment */
"github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/ga4gh" /* copybara-comment: ga4gh */
)
// contextKey is just an empty struct. It exists so RequestTTLInNanoFloat64 can be an immutable public variable with a unique type. It's immutable because nobody else can create a ContextKey, being unexported.
type contextKey struct{}
// RequestTTLInNanoFloat64 is the context key to use with golang.org/x/net/context's WithValue function to associate a "requested_ttl" value with a context.
var RequestTTLInNanoFloat64 contextKey
// valueType is the set of types which are treated like claims that have a
// value and source as sociated with them.
var valueType = map[reflect.Type]bool{
reflect.TypeOf([]ga4gh.OldClaim{}): true,
}
// ClaimValidator is a ga4gh.Validator that compares GA4GH claims.
type ClaimValidator struct {
Name string
ConstantMap map[string]bool
RegexValues []*regexp.Regexp
IsNot bool
Sources map[string]bool
By map[string]bool
}
// NewClaimValidator creates a ClaimValidator instance.
func NewClaimValidator(name string, values []string, is string, sources map[string]bool, by map[string]bool) (*ClaimValidator, error) {
rlist := []*regexp.Regexp{}
constMap := make(map[string]bool)
if len(is) > 0 && (is != "=" && is != "==" && is != "!=") {
return nil, fmt.Errorf("claim %q is %q comparison type is undefined", name, is)
}
for i, v := range values {
if len(v) == 0 {
return nil, fmt.Errorf("claim %q value %d is an empty string", name, i)
}
if v[0] == '^' {
// Treat as a regexp string.
if v[len(v)-1] != '$' {
return nil, fmt.Errorf("claim %q regular expression value %q is missing string terminator %q", name, v, "$")
}
re, err := regexp.Compile(v)
if err != nil {
return nil, fmt.Errorf("claim %q regular expression value %q error: %v", name, v, err)
}
rlist = append(rlist, re)
} else {
constMap[v] = true
}
}
return &ClaimValidator{
Name: name,
ConstantMap: constMap,
RegexValues: rlist,
IsNot: is == "!=",
Sources: sources,
By: by,
}, nil
}
func (c *ClaimValidator) Validate(ctx context.Context, identity *ga4gh.Identity) (bool, error) {
ttl, ok := ctx.Value(RequestTTLInNanoFloat64).(float64)
if !ok {
ttl = 0
}
ret := c.validate(ttl, identity)
if c.IsNot {
return !ret, nil
}
return ret, nil
}
func (c *ClaimValidator) validate(ttl float64, id *ga4gh.Identity) bool {
tnow := time.Now()
now := float64(tnow.Unix())
vs, ok := id.GA4GH[c.Name]
if !ok {
return false
}
for _, v := range vs {
if v.Asserted > now {
id.RejectVisa(v.VisaData, v.TokenFormat, "visa_before_active", "visa.asserted", "visa is not yet active (visa.asserted is in the future)")
continue
}
if v.Expires < now+ttl {
id.RejectVisa(v.VisaData, v.TokenFormat, "visa_expired", "exp", "visa expired")
continue
}
// GA4GH AAI requires that visas in AccessTokenVisaFormat need to verify their validity
// every hour. To adhere without rechecking, will only accept these visas for one hour
// from time of issue compared to the requested time of expiry of access (now+ttl).
if v.TokenFormat == ga4gh.AccessTokenVisaFormat {
requestedExpiry := tnow.Add(time.Duration(ttl * 1e9)) // ttl seconds to nano
iat := time.Unix(v.VisaData.IssuedAt, 0)
if requestedExpiry.Sub(iat) > time.Hour {
id.RejectVisa(v.VisaData, v.TokenFormat, "access_token_visa_expiry", "jku", "access token visa format not supported for access more than 1 hour, use document visa format via jku instead")
continue
}
}
match := false
if _, ok := c.ConstantMap[v.Value]; ok {
match = true
} else {
bv := []byte(v.Value)
for _, re := range c.RegexValues {
if re.Match(bv) {
match = true
break
}
}
}
if !match {
id.RejectVisa(v.VisaData, v.TokenFormat, "visa_value_rejected", "visa.value", fmt.Sprintf("visa value %q not accepted by the policy", v.Value))
continue
}
if len(v.Source) == 0 {
id.RejectVisa(v.VisaData, v.TokenFormat, "visa_source_missing", "visa.source", "visa source is empty")
continue
}
if len(c.Sources) > 0 {
if _, ok := c.Sources[v.Source]; !ok {
id.RejectVisa(v.VisaData, v.TokenFormat, "visa_source_rejected", "visa.source", fmt.Sprintf("visa source %q not accepted by the policy", v.Source))
continue
}
}
if len(c.By) > 0 {
if _, ok := c.By[v.By]; !ok {
id.RejectVisa(v.VisaData, v.TokenFormat, "visa_by_rejected", "visa.by", fmt.Sprintf("visa by %q not accepted by the policy", v.By))
continue
}
}
if len(v.Condition) == 0 {
return true
}
match = false
for ck, cv := range v.Condition {
match = false
idcList, ok := id.GA4GH[ck]
if !ok {
id.RejectVisa(v.VisaData, v.TokenFormat, "visa_by_rejected", "visa.by", fmt.Sprintf("visa by %q not accepted by the policy", v.By))
continue
}
for _, idc := range idcList {
if idc.Asserted > now || idc.Expires < now+ttl {
continue
}
if len(cv.Value) > 0 && !stringset.Contains(cv.Value, idc.Value) {
continue
}
if len(cv.Source) > 0 && !stringset.Contains(cv.Source, idc.Source) {
continue
}
if len(cv.By) > 0 && !stringset.Contains(cv.By, idc.By) {
continue
}
match = true
break
}
if !match {
break
}
}
if match {
return true
}
}
return false
}