diff --git a/api/api.go b/api/api.go index a92b7902..a4d09f91 100644 --- a/api/api.go +++ b/api/api.go @@ -36,6 +36,15 @@ type Authority interface { GetFederation() ([]*x509.Certificate, error) } +// TimeDuration is an alias of provisioner.TimeDuration +type TimeDuration = provisioner.TimeDuration + +// ParseTimeDuration returns a new TimeDuration parsing the RFC 3339 time or +// time.Duration string. +func ParseTimeDuration(s string) (TimeDuration, error) { + return provisioner.ParseTimeDuration(s) +} + // Certificate wraps a *x509.Certificate and adds the json.Marshaler interface. type Certificate struct { *x509.Certificate @@ -154,8 +163,8 @@ type RootResponse struct { type SignRequest struct { CsrPEM CertificateRequest `json:"csr"` OTT string `json:"ott"` - NotAfter time.Time `json:"notAfter"` - NotBefore time.Time `json:"notBefore"` + NotAfter TimeDuration `json:"notAfter"` + NotBefore TimeDuration `json:"notBefore"` } // ProvisionersResponse is the response object that returns the list of diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index c28fd80b..b8b4e51a 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -14,8 +14,8 @@ import ( // Options contains the options that can be passed to the Sign method. type Options struct { - NotAfter time.Time `json:"notAfter"` - NotBefore time.Time `json:"notBefore"` + NotAfter TimeDuration `json:"notAfter"` + NotBefore TimeDuration `json:"notBefore"` } // SignOption is the interface used to collect all extra options used in the @@ -55,7 +55,7 @@ func (v profileWithOption) Option(Options) x509util.WithOption { type profileDefaultDuration time.Duration func (v profileDefaultDuration) Option(so Options) x509util.WithOption { - return x509util.WithNotBeforeAfterDuration(so.NotBefore, so.NotAfter, time.Duration(v)) + return x509util.WithNotBeforeAfterDuration(so.NotBefore.Time(), so.NotAfter.Time(), time.Duration(v)) } // emailOnlyIdentity is a CertificateRequestValidator that checks that the only @@ -228,6 +228,6 @@ func createProvisionerExtension(typ int, name, credentialID string) (pkix.Extens } func init() { - // Avoid deadcode warning in profileWithOption + // Avoid dead-code warning in profileWithOption _ = profileWithOption(nil) } diff --git a/authority/provisioner/timeduration.go b/authority/provisioner/timeduration.go new file mode 100644 index 00000000..d447ead9 --- /dev/null +++ b/authority/provisioner/timeduration.go @@ -0,0 +1,109 @@ +package provisioner + +import ( + "encoding/json" + "time" + + "github.com/pkg/errors" +) + +// TimeDuration is a type that represents a time but the JSON unmarshaling can +// use a time using the RFC 3339 format or a time.Duration string. If a duration +// is used, the time will be set on the first call to TimeDuration.Time. +type TimeDuration struct { + t time.Time + d time.Duration +} + +// ParseTimeDuration returns a new TimeDuration parsing the RFC 3339 time or +// time.Duration string. +func ParseTimeDuration(s string) (TimeDuration, error) { + if s == "" { + return TimeDuration{}, nil + } + + // Try to use the unquoted RFC 3339 format + var t time.Time + if err := t.UnmarshalText([]byte(s)); err == nil { + return TimeDuration{t: t}, nil + } + + // Try to use the time.Duration string format + if d, err := time.ParseDuration(s); err == nil { + return TimeDuration{d: d}, nil + } + + return TimeDuration{}, errors.Errorf("failed to parse %s", s) +} + +// SetDuration initializes the TimeDuration with the given duration string. If +// the time was set it will re-set to zero. +func (t *TimeDuration) SetDuration(d time.Duration) { + t.t, t.d = time.Time{}, d +} + +// SetTime initializes the TimeDuration with the given time. If the duration is +// set it will be re-set to zero. +func (t *TimeDuration) SetTime(tt time.Time) { + t.t, t.d = tt, 0 +} + +// MarshalJSON implements the json.Marshaler interface. If the time is set it +// will return the time in RFC 3339 format if not it will return the duration +// string. +func (t *TimeDuration) MarshalJSON() ([]byte, error) { + switch { + case t == nil: + return []byte("null"), nil + case t.t.IsZero(): + if t.d == 0 { + return []byte("null"), nil + } + return json.Marshal(t.d.String()) + default: + return t.t.MarshalJSON() + } +} + +// UnmarshalJSON implements the json.Unmarshaler interface. The time is expected +// to be a quoted string in RFC 3339 format or a quoted time.Duration string. +func (t *TimeDuration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return errors.Wrapf(err, "error unmarshaling %s", data) + } + + // Empty TimeDuration + if s == "" { + *t = TimeDuration{} + return nil + } + + // Try to use the unquoted RFC 3339 format + var tt time.Time + if err := tt.UnmarshalText([]byte(s)); err == nil { + *t = TimeDuration{t: tt} + return nil + } + + // Try to use the time.Duration string format + if d, err := time.ParseDuration(s); err == nil { + *t = TimeDuration{d: d} + return nil + } + + return errors.Errorf("failed to parse %s", data) +} + +// Time set once the embedded time and returns it. +func (t *TimeDuration) Time() time.Time { + switch { + case t == nil: + return time.Time{} + case t.t.IsZero(): + t.t = time.Now().UTC().Add(t.d) + return t.t + default: + return t.t + } +}