Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 190cd1c

Browse files
stirbydannykoppingspikecurtis
authored
chore: apply fixes for the 2.15 release (#14540)
* Minor fixups, added troubleshooting (#14519) (cherry picked from commit 66c8060) * fix: allow posting licenses that will be valid in future (#14491) (cherry picked from commit 5bd5801) * fix: stop reporting future licenses as errors (#14492) (cherry picked from commit 4eac2ac) --------- Co-authored-by: Danny Kopping <[email protected]> Co-authored-by: Spike Curtis <[email protected]>
1 parent 0ef8514 commit 190cd1c

File tree

7 files changed

+156
-30
lines changed

7 files changed

+156
-30
lines changed

docs/admin/notifications.md

+17-6
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ can only be delivered to one method, and this method is configured globally with
7777
[`CODER_NOTIFICATIONS_METHOD`](https://coder.com/docs/reference/cli/server#--notifications-method)
7878
(default: `smtp`).
7979

80-
Enterprise customers can configured which method to use for each of the
81-
supported [Events](#events); see the [Preferences](#preferences) section below
82-
for more details.
80+
Enterprise customers can configure which method to use for each of the supported
81+
[Events](#events); see the [Preferences](#preferences) section below for more
82+
details.
8383

8484
## SMTP (Email)
8585

@@ -93,7 +93,7 @@ existing one.
9393
| :------: | --------------------------------- | ------------------------------------- | ----------- | ----------------------------------------- | ------------- |
9494
| ✔️ | `--notifications-email-from` | `CODER_NOTIFICATIONS_EMAIL_FROM` | `string` | The sender's address to use. | |
9595
| ✔️ | `--notifications-email-smarthost` | `CODER_NOTIFICATIONS_EMAIL_SMARTHOST` | `host:port` | The SMTP relay to send messages through. | localhost:587 |
96-
| -| `--notifications-email-hello` | `CODER_NOTIFICATIONS_EMAIL_HELLO` | `string` | The hostname identifying the SMTP server. | localhost |
96+
| | `--notifications-email-hello` | `CODER_NOTIFICATIONS_EMAIL_HELLO` | `string` | The hostname identifying the SMTP server. | localhost |
9797

9898
**Authentication Settings:**
9999

@@ -252,6 +252,18 @@ To pause sending notifications, execute
252252
To resume sending notifications, execute
253253
[`coder notifications resume`](https://coder.com/docs/reference/cli/notifications_resume).
254254

255+
## Troubleshooting
256+
257+
If notifications are not being delivered, use the following methods to
258+
troubleshoot:
259+
260+
1. Ensure notifications are being added to the `notification_messages` table
261+
2. Review any error messages in the `status_reason` column, should an error have
262+
occurred
263+
3. Review the logs (search for the term `notifications`) for diagnostic
264+
information<br> _If you do not see any relevant logs, set
265+
`CODER_VERBOSE=true` or `--verbose` to output debug logs_
266+
255267
## Internals
256268

257269
The notification system is built to operate concurrently in a single- or
@@ -288,5 +300,4 @@ messages._
288300
- after `CODER_NOTIFICATIONS_MAX_SEND_ATTEMPTS` is exceeded, it transitions to
289301
`permanent_failure`
290302

291-
Diagnostic messages will be saved in the `notification_messages` table and will
292-
be logged, in the case of failure.
303+
See [Troubleshooting](#troubleshooting) above for more details.
35 KB
Loading

enterprise/coderd/coderdenttest/coderdenttest.go

+15-1
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

@@ -195,6 +199,13 @@ func (opts *LicenseOptions) Valid(now time.Time) *LicenseOptions {
195199
return opts
196200
}
197201

202+
func (opts *LicenseOptions) FutureTerm(now time.Time) *LicenseOptions {
203+
opts.NotBefore = now.Add(time.Hour * 24)
204+
opts.ExpiresAt = now.Add(time.Hour * 24 * 60)
205+
opts.GraceAt = now.Add(time.Hour * 24 * 53)
206+
return opts
207+
}
208+
198209
func (opts *LicenseOptions) UserLimit(limit int64) *LicenseOptions {
199210
return opts.Feature(codersdk.FeatureUserLimit, limit)
200211
}
@@ -233,13 +244,16 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
233244
if options.GraceAt.IsZero() {
234245
options.GraceAt = time.Now().Add(time.Hour)
235246
}
247+
if options.NotBefore.IsZero() {
248+
options.NotBefore = time.Now().Add(-time.Minute)
249+
}
236250

237251
c := &license.Claims{
238252
RegisteredClaims: jwt.RegisteredClaims{
239253
ID: uuid.NewString(),
240254
Issuer: "[email protected]",
241255
ExpiresAt: jwt.NewNumericDate(options.ExpiresAt),
242-
NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Minute)),
256+
NotBefore: jwt.NewNumericDate(options.NotBefore),
243257
IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Minute)),
244258
},
245259
LicenseExpires: jwt.NewNumericDate(options.GraceAt),

enterprise/coderd/license/license.go

+46-2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ func LicensesEntitlements(
100100
// 'Entitlements' group as a whole.
101101
for _, license := range licenses {
102102
claims, err := ParseClaims(license.JWT, keys)
103+
var vErr *jwt.ValidationError
104+
if xerrors.As(err, &vErr) && vErr.Is(jwt.ErrTokenNotValidYet) {
105+
// The license isn't valid yet. We don't consider any entitlements contained in it, but
106+
// it's also not an error. Just skip it silently. This can happen if an administrator
107+
// uploads a license for a new term that hasn't started yet.
108+
continue
109+
}
103110
if err != nil {
104111
entitlements.Errors = append(entitlements.Errors,
105112
fmt.Sprintf("Invalid license (%s) parsing claims: %s", license.UUID.String(), err.Error()))
@@ -287,6 +294,8 @@ var (
287294
ErrInvalidVersion = xerrors.New("license must be version 3")
288295
ErrMissingKeyID = xerrors.Errorf("JOSE header must contain %s", HeaderKeyID)
289296
ErrMissingLicenseExpires = xerrors.New("license missing license_expires")
297+
ErrMissingExp = xerrors.New("exp claim missing or not parsable")
298+
ErrMultipleIssues = xerrors.New("license has multiple issues; contact support")
290299
)
291300

292301
type Features map[codersdk.FeatureName]int64
@@ -336,7 +345,7 @@ func ParseRaw(l string, keys map[string]ed25519.PublicKey) (jwt.MapClaims, error
336345
return nil, xerrors.New("unable to parse Claims")
337346
}
338347

339-
// ParseClaims validates a database.License record, and if valid, returns the claims. If
348+
// ParseClaims validates a raw JWT, and if valid, returns the claims. If
340349
// unparsable or invalid, it returns an error
341350
func ParseClaims(rawJWT string, keys map[string]ed25519.PublicKey) (*Claims, error) {
342351
tok, err := jwt.ParseWithClaims(
@@ -348,18 +357,53 @@ func ParseClaims(rawJWT string, keys map[string]ed25519.PublicKey) (*Claims, err
348357
if err != nil {
349358
return nil, err
350359
}
351-
if claims, ok := tok.Claims.(*Claims); ok && tok.Valid {
360+
return validateClaims(tok)
361+
}
362+
363+
func validateClaims(tok *jwt.Token) (*Claims, error) {
364+
if claims, ok := tok.Claims.(*Claims); ok {
352365
if claims.Version != uint64(CurrentVersion) {
353366
return nil, ErrInvalidVersion
354367
}
355368
if claims.LicenseExpires == nil {
356369
return nil, ErrMissingLicenseExpires
357370
}
371+
if claims.ExpiresAt == nil {
372+
return nil, ErrMissingExp
373+
}
358374
return claims, nil
359375
}
360376
return nil, xerrors.New("unable to parse Claims")
361377
}
362378

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

enterprise/coderd/license/license_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,25 @@ func TestLicenseEntitlements(t *testing.T) {
826826
assert.True(t, entitlements.Features[codersdk.FeatureCustomRoles].Enabled, "custom-roles enabled for premium")
827827
},
828828
},
829+
{
830+
Name: "CurrentAndFuture",
831+
Licenses: []*coderdenttest.LicenseOptions{
832+
enterpriseLicense().UserLimit(100),
833+
premiumLicense().UserLimit(200).FutureTerm(time.Now()),
834+
},
835+
Enablements: defaultEnablements,
836+
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
837+
assertEnterpriseFeatures(t, entitlements)
838+
assertNoErrors(t, entitlements)
839+
assertNoWarnings(t, entitlements)
840+
userFeature := entitlements.Features[codersdk.FeatureUserLimit]
841+
assert.Equalf(t, int64(100), *userFeature.Limit, "user limit")
842+
assert.Equal(t, codersdk.EntitlementNotEntitled,
843+
entitlements.Features[codersdk.FeatureMultipleOrganizations].Entitlement)
844+
assert.Equal(t, codersdk.EntitlementNotEntitled,
845+
entitlements.Features[codersdk.FeatureCustomRoles].Entitlement)
846+
},
847+
},
829848
}
830849

831850
for _, tc := range testCases {

enterprise/coderd/licenses.go

+11-21
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-
if err != 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
if err != 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
if err != 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+
if err != 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

+48
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
func TestGetLicense(t *testing.T) {

0 commit comments

Comments
 (0)