Skip to content

Commit 190cd1c

Browse files
stirbydannykoppingspikecurtis
authored
chore: apply fixes for the 2.15 release (coder#14540)
* Minor fixups, added troubleshooting (coder#14519) (cherry picked from commit 66c8060) * fix: allow posting licenses that will be valid in future (coder#14491) (cherry picked from commit 5bd5801) * fix: stop reporting future licenses as errors (coder#14492) (cherry picked from commit 4eac2ac) --------- Co-authored-by: Danny Kopping <danny@coder.com> Co-authored-by: Spike Curtis <spike@coder.com>
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

Lines changed: 17 additions & 6 deletions
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

Lines changed: 15 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

@@ -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: "test@testing.test",
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

Lines changed: 46 additions & 2 deletions
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

Lines changed: 19 additions & 0 deletions
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

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-
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

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

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy