feat: app sharing (now open source!) (#4378)

This commit is contained in:
Dean Sheather
2022-10-15 02:46:38 +10:00
committed by GitHub
parent 19d7281daf
commit d898737d6d
55 changed files with 1069 additions and 412 deletions

View File

@ -83,8 +83,8 @@ type OAuth2Configs struct {
}
const (
signedOutErrorMessage string = "You are signed out or your session has expired. Please sign in again to continue."
internalErrorMessage string = "An internal error occurred. Please try again or contact the system administrator."
SignedOutErrorMessage = "You are signed out or your session has expired. Please sign in again to continue."
internalErrorMessage = "An internal error occurred. Please try again or contact the system administrator."
)
type ExtractAPIKeyConfig struct {
@ -119,21 +119,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
// like workspace applications.
write := func(code int, response codersdk.Response) {
if cfg.RedirectToLogin {
path := r.URL.Path
if r.URL.RawQuery != "" {
path += "?" + r.URL.RawQuery
}
q := url.Values{}
q.Add("message", response.Message)
q.Add("redirect", path)
u := &url.URL{
Path: "/login",
RawQuery: q.Encode(),
}
http.Redirect(rw, r, u.String(), http.StatusTemporaryRedirect)
RedirectToLogin(rw, r, response.Message)
return
}
@ -157,7 +143,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
token := apiTokenFromRequest(r)
if token == "" {
optionalWrite(http.StatusUnauthorized, codersdk.Response{
Message: signedOutErrorMessage,
Message: SignedOutErrorMessage,
Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenKey),
})
return
@ -166,7 +152,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
keyID, keySecret, err := SplitAPIToken(token)
if err != nil {
optionalWrite(http.StatusUnauthorized, codersdk.Response{
Message: signedOutErrorMessage,
Message: SignedOutErrorMessage,
Detail: "Invalid API key format: " + err.Error(),
})
return
@ -176,7 +162,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
optionalWrite(http.StatusUnauthorized, codersdk.Response{
Message: signedOutErrorMessage,
Message: SignedOutErrorMessage,
Detail: "API key is invalid.",
})
return
@ -192,7 +178,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
hashedSecret := sha256.Sum256([]byte(keySecret))
if subtle.ConstantTimeCompare(key.HashedSecret, hashedSecret[:]) != 1 {
optionalWrite(http.StatusUnauthorized, codersdk.Response{
Message: signedOutErrorMessage,
Message: SignedOutErrorMessage,
Detail: "API key secret is invalid.",
})
return
@ -255,7 +241,7 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
// Checking if the key is expired.
if key.ExpiresAt.Before(now) {
optionalWrite(http.StatusUnauthorized, codersdk.Response{
Message: signedOutErrorMessage,
Message: SignedOutErrorMessage,
Detail: fmt.Sprintf("API key expired at %q.", key.ExpiresAt.String()),
})
return
@ -422,3 +408,23 @@ func SplitAPIToken(token string) (id string, secret string, err error) {
return keyID, keySecret, nil
}
// RedirectToLogin redirects the user to the login page with the `message` and
// `redirect` query parameters set.
func RedirectToLogin(rw http.ResponseWriter, r *http.Request, message string) {
path := r.URL.Path
if r.URL.RawQuery != "" {
path += "?" + r.URL.RawQuery
}
q := url.Values{}
q.Add("message", message)
q.Add("redirect", path)
u := &url.URL{
Path: "/login",
RawQuery: q.Encode(),
}
http.Redirect(rw, r, u.String(), http.StatusTemporaryRedirect)
}

View File

@ -148,7 +148,7 @@ func TestOrganizationParam(t *testing.T) {
DB: db,
RedirectToLogin: false,
}),
httpmw.ExtractUserParam(db),
httpmw.ExtractUserParam(db, false),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractOrganizationMemberParam(db),
)
@ -189,7 +189,7 @@ func TestOrganizationParam(t *testing.T) {
RedirectToLogin: false,
}),
httpmw.ExtractOrganizationParam(db),
httpmw.ExtractUserParam(db),
httpmw.ExtractUserParam(db, false),
httpmw.ExtractOrganizationMemberParam(db),
)
rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {

View File

@ -33,8 +33,11 @@ func UserParam(r *http.Request) database.User {
return user
}
// ExtractUserParam extracts a user from an ID/username in the {user} URL parameter.
func ExtractUserParam(db database.Store) func(http.Handler) http.Handler {
// ExtractUserParam extracts a user from an ID/username in the {user} URL
// parameter.
//
//nolint:revive
func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
var (
@ -53,7 +56,19 @@ func ExtractUserParam(db database.Store) func(http.Handler) http.Handler {
}
if userQuery == "me" {
user, err = db.GetUserByID(ctx, APIKey(r).UserID)
apiKey, ok := APIKeyOptional(r)
if !ok {
if redirectToLoginOnMe {
RedirectToLogin(rw, r, SignedOutErrorMessage)
return
}
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Cannot use \"me\" without a valid session.",
})
return
}
user, err = db.GetUserByID(ctx, apiKey.UserID)
if xerrors.Is(err, sql.ErrNoRows) {
httpapi.ResourceNotFound(rw)
return

View File

@ -63,7 +63,7 @@ func TestUserParam(t *testing.T) {
r = returnedRequest
})).ServeHTTP(rw, r)
httpmw.ExtractUserParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
httpmw.ExtractUserParam(db, false)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
})).ServeHTTP(rw, r)
res := rw.Result()
@ -85,7 +85,7 @@ func TestUserParam(t *testing.T) {
routeContext := chi.NewRouteContext()
routeContext.URLParams.Add("user", "ben")
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
httpmw.ExtractUserParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
httpmw.ExtractUserParam(db, false)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
})).ServeHTTP(rw, r)
res := rw.Result()
@ -107,7 +107,7 @@ func TestUserParam(t *testing.T) {
routeContext := chi.NewRouteContext()
routeContext.URLParams.Add("user", "me")
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
httpmw.ExtractUserParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
httpmw.ExtractUserParam(db, false)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
_ = httpmw.UserParam(r)
rw.WriteHeader(http.StatusOK)
})).ServeHTTP(rw, r)

View File

@ -305,7 +305,7 @@ func TestWorkspaceAgentByNameParam(t *testing.T) {
DB: db,
RedirectToLogin: true,
}),
httpmw.ExtractUserParam(db),
httpmw.ExtractUserParam(db, false),
httpmw.ExtractWorkspaceAndAgentParam(db),
)
rtr.Get("/", func(w http.ResponseWriter, r *http.Request) {