Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5bd5801

Browse files
authored
fix: allow posting licenses that will be valid in future (#14491)
1 parent0785b77 commit5bd5801

File tree

4 files changed

+106
-24
lines changed

4 files changed

+106
-24
lines changed

‎enterprise/coderd/coderdenttest/coderdenttest.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ type LicenseOptions struct {
174174
// ExpiresAt is the time at which the license will hard expire.
175175
// ExpiresAt should always be greater then GraceAt.
176176
ExpiresAt time.Time
177+
// NotBefore is the time at which the license becomes valid. If set to the
178+
// zero value, the `nbf` claim on the license is set to 1 minute in the
179+
// past.
180+
NotBefore time.Time
177181
Features license.Features
178182
}
179183

@@ -233,13 +237,16 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
233237
ifoptions.GraceAt.IsZero() {
234238
options.GraceAt=time.Now().Add(time.Hour)
235239
}
240+
ifoptions.NotBefore.IsZero() {
241+
options.NotBefore=time.Now().Add(-time.Minute)
242+
}
236243

237244
c:=&license.Claims{
238245
RegisteredClaims: jwt.RegisteredClaims{
239246
ID:uuid.NewString(),
240247
Issuer:"test@testing.test",
241248
ExpiresAt:jwt.NewNumericDate(options.ExpiresAt),
242-
NotBefore:jwt.NewNumericDate(time.Now().Add(-time.Minute)),
249+
NotBefore:jwt.NewNumericDate(options.NotBefore),
243250
IssuedAt:jwt.NewNumericDate(time.Now().Add(-time.Minute)),
244251
},
245252
LicenseExpires:jwt.NewNumericDate(options.GraceAt),

‎enterprise/coderd/license/license.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ var (
287287
ErrInvalidVersion=xerrors.New("license must be version 3")
288288
ErrMissingKeyID=xerrors.Errorf("JOSE header must contain %s",HeaderKeyID)
289289
ErrMissingLicenseExpires=xerrors.New("license missing license_expires")
290+
ErrMissingExp=xerrors.New("exp claim missing or not parsable")
291+
ErrMultipleIssues=xerrors.New("license has multiple issues; contact support")
290292
)
291293

292294
typeFeaturesmap[codersdk.FeatureName]int64
@@ -336,7 +338,7 @@ func ParseRaw(l string, keys map[string]ed25519.PublicKey) (jwt.MapClaims, error
336338
returnnil,xerrors.New("unable to parse Claims")
337339
}
338340

339-
// ParseClaims validates adatabase.License record, and if valid, returns the claims. If
341+
// ParseClaims validates araw JWT, and if valid, returns the claims. If
340342
// unparsable or invalid, it returns an error
341343
funcParseClaims(rawJWTstring,keysmap[string]ed25519.PublicKey) (*Claims,error) {
342344
tok,err:=jwt.ParseWithClaims(
@@ -348,18 +350,53 @@ func ParseClaims(rawJWT string, keys map[string]ed25519.PublicKey) (*Claims, err
348350
iferr!=nil {
349351
returnnil,err
350352
}
351-
ifclaims,ok:=tok.Claims.(*Claims);ok&&tok.Valid {
353+
returnvalidateClaims(tok)
354+
}
355+
356+
funcvalidateClaims(tok*jwt.Token) (*Claims,error) {
357+
ifclaims,ok:=tok.Claims.(*Claims);ok {
352358
ifclaims.Version!=uint64(CurrentVersion) {
353359
returnnil,ErrInvalidVersion
354360
}
355361
ifclaims.LicenseExpires==nil {
356362
returnnil,ErrMissingLicenseExpires
357363
}
364+
ifclaims.ExpiresAt==nil {
365+
returnnil,ErrMissingExp
366+
}
358367
returnclaims,nil
359368
}
360369
returnnil,xerrors.New("unable to parse Claims")
361370
}
362371

372+
// ParseClaimsIgnoreNbf validates a raw JWT, but ignores `nbf` claim. If otherwise valid, it returns
373+
// the claims. If unparsable or invalid, it returns an error. Ignoring the `nbf` (not before) is
374+
// useful to determine if a JWT _will_ become valid at any point now or in the future.
375+
funcParseClaimsIgnoreNbf(rawJWTstring,keysmap[string]ed25519.PublicKey) (*Claims,error) {
376+
tok,err:=jwt.ParseWithClaims(
377+
rawJWT,
378+
&Claims{},
379+
keyFunc(keys),
380+
jwt.WithValidMethods(ValidMethods),
381+
)
382+
varvErr*jwt.ValidationError
383+
ifxerrors.As(err,&vErr) {
384+
// zero out the NotValidYet error to check if there were other problems
385+
vErr.Errors=vErr.Errors& (^jwt.ValidationErrorNotValidYet)
386+
ifvErr.Errors!=0 {
387+
// There are other errors besides not being valid yet. We _could_ go
388+
// through all the jwt.ValidationError bits and try to work out the
389+
// correct error, but if we get here something very strange is
390+
// going on so let's just return a generic error that says to get in
391+
// touch with our support team.
392+
returnnil,ErrMultipleIssues
393+
}
394+
}elseiferr!=nil {
395+
returnnil,err
396+
}
397+
returnvalidateClaims(tok)
398+
}
399+
363400
funckeyFunc(keysmap[string]ed25519.PublicKey)func(*jwt.Token) (interface{},error) {
364401
returnfunc(j*jwt.Token) (interface{},error) {
365402
keyID,ok:=j.Header[HeaderKeyID].(string)

‎enterprise/coderd/licenses.go

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -86,25 +86,7 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
8686
return
8787
}
8888

89-
rawClaims,err:=license.ParseRaw(addLicense.License,api.LicenseKeys)
90-
iferr!=nil {
91-
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
92-
Message:"Invalid license",
93-
Detail:err.Error(),
94-
})
95-
return
96-
}
97-
exp,ok:=rawClaims["exp"].(float64)
98-
if!ok {
99-
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
100-
Message:"Invalid license",
101-
Detail:"exp claim missing or not parsable",
102-
})
103-
return
104-
}
105-
expTime:=time.Unix(int64(exp),0)
106-
107-
claims,err:=license.ParseClaims(addLicense.License,api.LicenseKeys)
89+
claims,err:=license.ParseClaimsIgnoreNbf(addLicense.License,api.LicenseKeys)
10890
iferr!=nil {
10991
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
11092
Message:"Invalid license",
@@ -134,7 +116,7 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
134116
dl,err:=api.Database.InsertLicense(ctx, database.InsertLicenseParams{
135117
UploadedAt:dbtime.Now(),
136118
JWT:addLicense.License,
137-
Exp:expTime,
119+
Exp:claims.ExpiresAt.Time,
138120
UUID:id,
139121
})
140122
iferr!=nil {
@@ -160,7 +142,15 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
160142
// don't fail the HTTP request, since we did write it successfully to the database
161143
}
162144

163-
httpapi.Write(ctx,rw,http.StatusCreated,convertLicense(dl,rawClaims))
145+
c,err:=decodeClaims(dl)
146+
iferr!=nil {
147+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
148+
Message:"Failed to decode database response",
149+
Detail:err.Error(),
150+
})
151+
return
152+
}
153+
httpapi.Write(ctx,rw,http.StatusCreated,convertLicense(dl,c))
164154
}
165155

166156
// postRefreshEntitlements forces an `updateEntitlements` call and publishes

‎enterprise/coderd/licenses_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"net/http"
66
"testing"
7+
"time"
78

89
"github.com/google/uuid"
910
"github.com/stretchr/testify/assert"
@@ -82,6 +83,53 @@ func TestPostLicense(t *testing.T) {
8283
t.Error("expected to get error status 400")
8384
}
8485
})
86+
87+
// Test a license that isn't yet valid, but will be in the future. We should allow this so that
88+
// operators can upload a license ahead of time.
89+
t.Run("NotYet",func(t*testing.T) {
90+
t.Parallel()
91+
client,_:=coderdenttest.New(t,&coderdenttest.Options{DontAddLicense:true})
92+
respLic:=coderdenttest.AddLicense(t,client, coderdenttest.LicenseOptions{
93+
AccountType:license.AccountTypeSalesforce,
94+
AccountID:"testing",
95+
Features: license.Features{
96+
codersdk.FeatureAuditLog:1,
97+
},
98+
NotBefore:time.Now().Add(time.Hour),
99+
GraceAt:time.Now().Add(2*time.Hour),
100+
ExpiresAt:time.Now().Add(3*time.Hour),
101+
})
102+
assert.GreaterOrEqual(t,respLic.ID,int32(0))
103+
// just a couple spot checks for sanity
104+
assert.Equal(t,"testing",respLic.Claims["account_id"])
105+
features,err:=respLic.FeaturesClaims()
106+
require.NoError(t,err)
107+
assert.EqualValues(t,1,features[codersdk.FeatureAuditLog])
108+
})
109+
110+
// Test we still reject a license that isn't valid yet, but has other issues (e.g. expired
111+
// before it starts).
112+
t.Run("NotEver",func(t*testing.T) {
113+
t.Parallel()
114+
client,_:=coderdenttest.New(t,&coderdenttest.Options{DontAddLicense:true})
115+
lic:=coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
116+
AccountType:license.AccountTypeSalesforce,
117+
AccountID:"testing",
118+
Features: license.Features{
119+
codersdk.FeatureAuditLog:1,
120+
},
121+
NotBefore:time.Now().Add(time.Hour),
122+
GraceAt:time.Now().Add(2*time.Hour),
123+
ExpiresAt:time.Now().Add(-time.Hour),
124+
})
125+
_,err:=client.AddLicense(context.Background(), codersdk.AddLicenseRequest{
126+
License:lic,
127+
})
128+
errResp:=&codersdk.Error{}
129+
require.ErrorAs(t,err,&errResp)
130+
require.Equal(t,http.StatusBadRequest,errResp.StatusCode())
131+
require.Contains(t,errResp.Detail,license.ErrMultipleIssues.Error())
132+
})
85133
}
86134

87135
funcTestGetLicense(t*testing.T) {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp