nuts-foundation/nuts-auth

View on GitHub
pkg/contract/contract.go

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * Nuts auth
 * Copyright (C) 2020. Nuts community
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package contract

import (
    "fmt"
    "regexp"
    "time"

    "github.com/goodsign/monday"
)

// Contract contains the contract template, the raw contract text and the extracted params.
type Contract struct {
    RawContractText string
    Template        *Template
    Params          map[string]string
}

// ParseContractString parses a raw string, finds the contract from the store and extracts the params
// Note: It does not verify the params
func ParseContractString(rawContractText string, contractTemplates TemplateStore) (*Contract, error) {

    // first, find the contract Template
    template, err := contractTemplates.FindFromRawContractText(rawContractText)
    if err != nil {
        return nil, err
    }

    if template == nil {
        return nil, ErrContractNotFound
    }

    contract := &Contract{
        Template:        template,
        RawContractText: rawContractText,
    }

    if err = contract.initParams(); err != nil {
        return nil, err
    }

    return contract, nil
}

func (sc *Contract) initParams() error {
    // extract the params
    r, _ := regexp.Compile(sc.Template.Regexp)
    matchResult := r.FindSubmatch([]byte(sc.RawContractText))
    if len(matchResult) < 1 {
        return fmt.Errorf("%w: could not match the contract template regex", ErrInvalidContractText)
    }
    matches := matchResult[1:]

    if len(matches) != len(sc.Template.TemplateAttributes) {
        return fmt.Errorf("%w: amount of template attributes does not match the amount of Params: found: %d, expected %d", ErrInvalidContractText, len(matches), len(sc.Template.TemplateAttributes))
    }

    sc.Params = make(map[string]string, len(matches))
    for idx, match := range matches {
        sc.Params[sc.Template.TemplateAttributes[idx]] = string(match)
    }
    return nil
}

var ErrInvalidPeriod = fmt.Errorf("%w: invalid period", ErrInvalidContractText)

// VerifyForGivenTime checks if the contract is valid for the given moment in time
// TODO: support of different time zones: https://github.com/nuts-foundation/nuts-auth/issues/152
func (sc Contract) VerifyForGivenTime(checkTime time.Time) error {
    var (
        err                      error
        ok                       bool
        validFrom, validTo       *time.Time
        validFromStr, validToStr string
    )

    if validFromStr, ok = sc.Params[ValidFromAttr]; !ok {
        return fmt.Errorf("%w: value for [%s] is missing", ErrInvalidContractText, ValidFromAttr)
    }

    validFrom, err = parseTime(validFromStr, sc.Template.Language)
    if err != nil {
        return fmt.Errorf("%w: unable to parse [%s]: %s", ErrInvalidContractText, ValidFromAttr, err)

    }

    if validToStr, ok = sc.Params[ValidToAttr]; !ok {
        return fmt.Errorf("%w: value for [%s] is missing", ErrInvalidContractText, ValidToAttr)
    }

    validTo, err = parseTime(validToStr, sc.Template.Language)
    if err != nil {
        return fmt.Errorf("%w: unable to parse [%s]: %s", ErrInvalidContractText, ValidToAttr, err)
    }

    // All parsed, check time range
    if validFrom.After(*validTo) {
        return fmt.Errorf("%w: [%s] must become before [%s]", ErrInvalidPeriod, ValidFromAttr, ValidToAttr)
    }

    amsterdamLocation, _ := time.LoadLocation(AmsterdamTimeZone)
    if checkTime.In(amsterdamLocation).Before(*validFrom) {
        return fmt.Errorf("%w: contract is not yet valid", ErrInvalidPeriod)
    }
    if checkTime.In(amsterdamLocation).After(*validTo) {
        return fmt.Errorf("%w: contract is expired", ErrInvalidPeriod)
    }

    return nil
}

// Verify verifies the params with the template
func (sc Contract) Verify() error {
    now := NowFunc()
    return sc.VerifyForGivenTime(now)
}

// parseTime parses the given timeStr in context of the Europe/Amsterdam time zone and uses the given language.
// Note that currently only the language "NL" is supported.
// TODO: support of different time zones: https://github.com/nuts-foundation/nuts-auth/issues/152
func parseTime(timeStr string, _ Language) (*time.Time, error) {
    contractIssuerTimezone, _ := time.LoadLocation(AmsterdamTimeZone)
    // TODO: add support for other languages
    parsedTime, err := monday.ParseInLocation(timeLayout, timeStr, contractIssuerTimezone, monday.LocaleNlNL)
    if err != nil {
        return nil, fmt.Errorf("invalid time string [%v]: %w", timeStr, err)
    }
    return &parsedTime, nil
}

const AmsterdamTimeZone = "Europe/Amsterdam"