mirror of https://github.com/coder/coder.git
feat: app sharing (now open source!) (#4378)
This commit is contained in:
parent
19d7281daf
commit
d898737d6d
|
@ -55,7 +55,7 @@ func createToken() *cobra.Command {
|
|||
return xerrors.Errorf("create codersdk client: %w", err)
|
||||
}
|
||||
|
||||
res, err := client.CreateToken(cmd.Context(), codersdk.Me)
|
||||
res, err := client.CreateToken(cmd.Context(), codersdk.Me, codersdk.CreateTokenRequest{})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create tokens: %w", err)
|
||||
}
|
||||
|
|
|
@ -34,12 +34,23 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
var createToken codersdk.CreateTokenRequest
|
||||
if !httpapi.Read(ctx, rw, r, &createToken) {
|
||||
return
|
||||
}
|
||||
|
||||
scope := database.APIKeyScopeAll
|
||||
if scope != "" {
|
||||
scope = database.APIKeyScope(createToken.Scope)
|
||||
}
|
||||
|
||||
// tokens last 100 years
|
||||
lifeTime := time.Hour * 876000
|
||||
cookie, err := api.createAPIKey(ctx, createAPIKeyParams{
|
||||
UserID: user.ID,
|
||||
LoginType: database.LoginTypeToken,
|
||||
ExpiresAt: database.Now().Add(lifeTime),
|
||||
Scope: scope,
|
||||
LifetimeSeconds: int64(lifeTime.Seconds()),
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -54,6 +65,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Creates a new session key, used for logging in via the CLI.
|
||||
// DEPRECATED: use postToken instead.
|
||||
func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := httpmw.UserParam(r)
|
||||
|
@ -229,6 +241,11 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h
|
|||
if params.Scope != "" {
|
||||
scope = params.Scope
|
||||
}
|
||||
switch scope {
|
||||
case database.APIKeyScopeAll, database.APIKeyScopeApplicationConnect:
|
||||
default:
|
||||
return nil, xerrors.Errorf("invalid API key scope: %q", scope)
|
||||
}
|
||||
|
||||
key, err := api.Database.InsertAPIKey(ctx, database.InsertAPIKeyParams{
|
||||
ID: keyID,
|
||||
|
|
|
@ -14,30 +14,61 @@ import (
|
|||
|
||||
func TestTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
keys, err := client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, keys)
|
||||
|
||||
res, err := client.CreateToken(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(res.Key), 2)
|
||||
t.Run("CRUD", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keys, err = client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, len(keys), 1)
|
||||
require.Contains(t, res.Key, keys[0].ID)
|
||||
// expires_at must be greater than 50 years
|
||||
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
keys, err := client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, keys)
|
||||
|
||||
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
|
||||
require.NoError(t, err)
|
||||
keys, err = client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, keys)
|
||||
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(res.Key), 2)
|
||||
|
||||
keys, err = client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, len(keys), 1)
|
||||
require.Contains(t, res.Key, keys[0].ID)
|
||||
// expires_at must be greater than 50 years
|
||||
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
|
||||
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
|
||||
|
||||
// no update
|
||||
|
||||
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
|
||||
require.NoError(t, err)
|
||||
keys, err = client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, keys)
|
||||
})
|
||||
|
||||
t.Run("Scoped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
|
||||
Scope: codersdk.APIKeyScopeApplicationConnect,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(res.Key), 2)
|
||||
|
||||
keys, err := client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, len(keys), 1)
|
||||
require.Contains(t, res.Key, keys[0].ID)
|
||||
// expires_at must be greater than 50 years
|
||||
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
|
||||
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIKey(t *testing.T) {
|
||||
|
|
|
@ -197,7 +197,7 @@ func New(options *Options) *API {
|
|||
RedirectToLogin: false,
|
||||
Optional: true,
|
||||
}),
|
||||
httpmw.ExtractUserParam(api.Database),
|
||||
httpmw.ExtractUserParam(api.Database, false),
|
||||
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
|
||||
),
|
||||
// Build-Version is helpful for debugging.
|
||||
|
@ -214,8 +214,18 @@ func New(options *Options) *API {
|
|||
r.Use(
|
||||
tracing.Middleware(api.TracerProvider),
|
||||
httpmw.RateLimitPerMinute(options.APIRateLimit),
|
||||
apiKeyMiddlewareRedirect,
|
||||
httpmw.ExtractUserParam(api.Database),
|
||||
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
|
||||
DB: options.Database,
|
||||
OAuth2Configs: oauthConfigs,
|
||||
// Optional is true to allow for public apps. If an
|
||||
// authorization check fails and the user is not authenticated,
|
||||
// they will be redirected to the login page by the app handler.
|
||||
RedirectToLogin: false,
|
||||
Optional: true,
|
||||
}),
|
||||
// Redirect to the login page if the user tries to open an app with
|
||||
// "me" as the username and they are not logged in.
|
||||
httpmw.ExtractUserParam(api.Database, true),
|
||||
// Extracts the <workspace.agent> from the url
|
||||
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
|
||||
)
|
||||
|
@ -310,7 +320,7 @@ func New(options *Options) *API {
|
|||
r.Get("/roles", api.assignableOrgRoles)
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractUserParam(options.Database),
|
||||
httpmw.ExtractUserParam(options.Database, false),
|
||||
httpmw.ExtractOrganizationMemberParam(options.Database),
|
||||
)
|
||||
r.Put("/roles", api.putMemberRoles)
|
||||
|
@ -389,7 +399,7 @@ func New(options *Options) *API {
|
|||
r.Get("/", api.assignableSiteRoles)
|
||||
})
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractUserParam(options.Database))
|
||||
r.Use(httpmw.ExtractUserParam(options.Database, false))
|
||||
r.Delete("/", api.deleteUser)
|
||||
r.Get("/", api.userByName)
|
||||
r.Put("/profile", api.putUserProfile)
|
||||
|
|
|
@ -2324,6 +2324,10 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW
|
|||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
if arg.SharingLevel == "" {
|
||||
arg.SharingLevel = database.AppSharingLevelOwner
|
||||
}
|
||||
|
||||
// nolint:gosimple
|
||||
workspaceApp := database.WorkspaceApp{
|
||||
ID: arg.ID,
|
||||
|
@ -2334,6 +2338,7 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW
|
|||
Command: arg.Command,
|
||||
Url: arg.Url,
|
||||
Subdomain: arg.Subdomain,
|
||||
SharingLevel: arg.SharingLevel,
|
||||
HealthcheckUrl: arg.HealthcheckUrl,
|
||||
HealthcheckInterval: arg.HealthcheckInterval,
|
||||
HealthcheckThreshold: arg.HealthcheckThreshold,
|
||||
|
|
|
@ -5,6 +5,12 @@ CREATE TYPE api_key_scope AS ENUM (
|
|||
'application_connect'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
'owner',
|
||||
'authenticated',
|
||||
'public'
|
||||
);
|
||||
|
||||
CREATE TYPE audit_action AS ENUM (
|
||||
'create',
|
||||
'write',
|
||||
|
@ -371,7 +377,8 @@ CREATE TABLE workspace_apps (
|
|||
healthcheck_interval integer DEFAULT 0 NOT NULL,
|
||||
healthcheck_threshold integer DEFAULT 0 NOT NULL,
|
||||
health workspace_app_health DEFAULT 'disabled'::public.workspace_app_health NOT NULL,
|
||||
subdomain boolean DEFAULT false NOT NULL
|
||||
subdomain boolean DEFAULT false NOT NULL,
|
||||
sharing_level app_sharing_level DEFAULT 'owner'::public.app_sharing_level NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE workspace_builds (
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
-- Drop column sharing_level from workspace_apps
|
||||
ALTER TABLE workspace_apps DROP COLUMN sharing_level;
|
||||
|
||||
-- Drop type app_sharing_level
|
||||
DROP TYPE app_sharing_level;
|
|
@ -0,0 +1,12 @@
|
|||
-- Add enum app_sharing_level
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
-- only the workspace owner can access the app
|
||||
'owner',
|
||||
-- any authenticated user on the site can access the app
|
||||
'authenticated',
|
||||
-- any user can access the app even if they are not authenticated
|
||||
'public'
|
||||
);
|
||||
|
||||
-- Add sharing_level column to workspace_apps table
|
||||
ALTER TABLE workspace_apps ADD COLUMN sharing_level app_sharing_level NOT NULL DEFAULT 'owner'::app_sharing_level;
|
|
@ -34,6 +34,26 @@ func (e *APIKeyScope) Scan(src interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type AppSharingLevel string
|
||||
|
||||
const (
|
||||
AppSharingLevelOwner AppSharingLevel = "owner"
|
||||
AppSharingLevelAuthenticated AppSharingLevel = "authenticated"
|
||||
AppSharingLevelPublic AppSharingLevel = "public"
|
||||
)
|
||||
|
||||
func (e *AppSharingLevel) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = AppSharingLevel(s)
|
||||
case string:
|
||||
*e = AppSharingLevel(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for AppSharingLevel: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuditAction string
|
||||
|
||||
const (
|
||||
|
@ -626,6 +646,7 @@ type WorkspaceApp struct {
|
|||
HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"`
|
||||
Health WorkspaceAppHealth `db:"health" json:"health"`
|
||||
Subdomain bool `db:"subdomain" json:"subdomain"`
|
||||
SharingLevel AppSharingLevel `db:"sharing_level" json:"sharing_level"`
|
||||
}
|
||||
|
||||
type WorkspaceBuild struct {
|
||||
|
|
|
@ -4324,7 +4324,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentVersionByID(ctx context.Context, arg Up
|
|||
}
|
||||
|
||||
const getWorkspaceAppByAgentIDAndName = `-- name: GetWorkspaceAppByAgentIDAndName :one
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain FROM workspace_apps WHERE agent_id = $1 AND name = $2
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE agent_id = $1 AND name = $2
|
||||
`
|
||||
|
||||
type GetWorkspaceAppByAgentIDAndNameParams struct {
|
||||
|
@ -4348,12 +4348,13 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndName(ctx context.Context, arg Ge
|
|||
&i.HealthcheckThreshold,
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE agent_id = $1 ORDER BY name ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) {
|
||||
|
@ -4378,6 +4379,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid
|
|||
&i.HealthcheckThreshold,
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -4393,7 +4395,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid
|
|||
}
|
||||
|
||||
const getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY name ASC
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY name ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) {
|
||||
|
@ -4418,6 +4420,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.
|
|||
&i.HealthcheckThreshold,
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -4433,7 +4436,7 @@ func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.
|
|||
}
|
||||
|
||||
const getWorkspaceAppsCreatedAfter = `-- name: GetWorkspaceAppsCreatedAfter :many
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC
|
||||
SELECT id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level FROM workspace_apps WHERE created_at > $1 ORDER BY name ASC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) {
|
||||
|
@ -4458,6 +4461,7 @@ func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt
|
|||
&i.HealthcheckThreshold,
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -4483,13 +4487,14 @@ INSERT INTO
|
|||
command,
|
||||
url,
|
||||
subdomain,
|
||||
sharing_level,
|
||||
healthcheck_url,
|
||||
healthcheck_interval,
|
||||
healthcheck_threshold,
|
||||
health
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, agent_id, name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level
|
||||
`
|
||||
|
||||
type InsertWorkspaceAppParams struct {
|
||||
|
@ -4501,6 +4506,7 @@ type InsertWorkspaceAppParams struct {
|
|||
Command sql.NullString `db:"command" json:"command"`
|
||||
Url sql.NullString `db:"url" json:"url"`
|
||||
Subdomain bool `db:"subdomain" json:"subdomain"`
|
||||
SharingLevel AppSharingLevel `db:"sharing_level" json:"sharing_level"`
|
||||
HealthcheckUrl string `db:"healthcheck_url" json:"healthcheck_url"`
|
||||
HealthcheckInterval int32 `db:"healthcheck_interval" json:"healthcheck_interval"`
|
||||
HealthcheckThreshold int32 `db:"healthcheck_threshold" json:"healthcheck_threshold"`
|
||||
|
@ -4517,6 +4523,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace
|
|||
arg.Command,
|
||||
arg.Url,
|
||||
arg.Subdomain,
|
||||
arg.SharingLevel,
|
||||
arg.HealthcheckUrl,
|
||||
arg.HealthcheckInterval,
|
||||
arg.HealthcheckThreshold,
|
||||
|
@ -4536,6 +4543,7 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace
|
|||
&i.HealthcheckThreshold,
|
||||
&i.Health,
|
||||
&i.Subdomain,
|
||||
&i.SharingLevel,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
@ -21,13 +21,14 @@ INSERT INTO
|
|||
command,
|
||||
url,
|
||||
subdomain,
|
||||
sharing_level,
|
||||
healthcheck_url,
|
||||
healthcheck_interval,
|
||||
healthcheck_threshold,
|
||||
health
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *;
|
||||
|
||||
-- name: UpdateWorkspaceAppHealthByID :exec
|
||||
UPDATE
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -814,6 +814,14 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
|||
health = database.WorkspaceAppHealthInitializing
|
||||
}
|
||||
|
||||
sharingLevel := database.AppSharingLevelOwner
|
||||
switch app.SharingLevel {
|
||||
case sdkproto.AppSharingLevel_AUTHENTICATED:
|
||||
sharingLevel = database.AppSharingLevelAuthenticated
|
||||
case sdkproto.AppSharingLevel_PUBLIC:
|
||||
sharingLevel = database.AppSharingLevelPublic
|
||||
}
|
||||
|
||||
dbApp, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
|
@ -829,6 +837,7 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
|
|||
Valid: app.Url != "",
|
||||
},
|
||||
Subdomain: app.Subdomain,
|
||||
SharingLevel: sharingLevel,
|
||||
HealthcheckUrl: app.Healthcheck.Url,
|
||||
HealthcheckInterval: app.Healthcheck.Interval,
|
||||
HealthcheckThreshold: app.Healthcheck.Threshold,
|
||||
|
|
|
@ -1207,6 +1207,7 @@ func convertAPIKey(k database.APIKey) codersdk.APIKey {
|
|||
CreatedAt: k.CreatedAt,
|
||||
UpdatedAt: k.UpdatedAt,
|
||||
LoginType: codersdk.LoginType(k.LoginType),
|
||||
Scope: codersdk.APIKeyScope(k.Scope),
|
||||
LifetimeSeconds: k.LifetimeSeconds,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -286,7 +286,7 @@ func TestPostLogin(t *testing.T) {
|
|||
require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400")
|
||||
|
||||
// tokens have a longer life
|
||||
token, err := client.CreateToken(ctx, codersdk.Me)
|
||||
token, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
|
||||
require.NoError(t, err, "make new token api key")
|
||||
split = strings.Split(token.Key, "-")
|
||||
apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0])
|
||||
|
@ -1202,7 +1202,7 @@ func TestPostTokens(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
apiKey, err := client.CreateToken(ctx, codersdk.Me)
|
||||
apiKey, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
|
||||
require.NotNil(t, apiKey)
|
||||
require.GreaterOrEqual(t, len(apiKey.Key), 2)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -594,11 +594,12 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp {
|
|||
apps := make([]codersdk.WorkspaceApp, 0)
|
||||
for _, dbApp := range dbApps {
|
||||
apps = append(apps, codersdk.WorkspaceApp{
|
||||
ID: dbApp.ID,
|
||||
Name: dbApp.Name,
|
||||
Command: dbApp.Command.String,
|
||||
Icon: dbApp.Icon,
|
||||
Subdomain: dbApp.Subdomain,
|
||||
ID: dbApp.ID,
|
||||
Name: dbApp.Name,
|
||||
Command: dbApp.Command.String,
|
||||
Icon: dbApp.Icon,
|
||||
Subdomain: dbApp.Subdomain,
|
||||
SharingLevel: codersdk.WorkspaceAppSharingLevel(dbApp.SharingLevel),
|
||||
Healthcheck: codersdk.Healthcheck{
|
||||
URL: dbApp.HealthcheckUrl,
|
||||
Interval: dbApp.HealthcheckInterval,
|
||||
|
|
|
@ -16,10 +16,12 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/xerrors"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
|
@ -32,8 +34,6 @@ import (
|
|||
const (
|
||||
// This needs to be a super unique query parameter because we don't want to
|
||||
// conflict with query parameters that users may use.
|
||||
// TODO: this will make dogfooding harder so come up with a more unique
|
||||
// solution
|
||||
//nolint:gosec
|
||||
subdomainProxyAPIKeyParam = "coder_application_connect_api_key_35e783"
|
||||
redirectURIQueryParam = "redirect_uri"
|
||||
|
@ -51,8 +51,32 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
|||
workspace := httpmw.WorkspaceParam(r)
|
||||
agent := httpmw.WorkspaceAgentParam(r)
|
||||
|
||||
if !api.Authorize(r, rbac.ActionCreate, workspace.ApplicationConnectRBAC()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
// We do not support port proxying on paths, so lookup the app by name.
|
||||
appName := chi.URLParam(r, "workspaceapp")
|
||||
app, ok := api.lookupWorkspaceApp(rw, r, agent.ID, appName)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
appSharingLevel := database.AppSharingLevelOwner
|
||||
if app.SharingLevel != "" {
|
||||
appSharingLevel = app.SharingLevel
|
||||
}
|
||||
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !authed {
|
||||
_, hasAPIKey := httpmw.APIKeyOptional(r)
|
||||
if hasAPIKey {
|
||||
// The request has a valid API key but insufficient permissions.
|
||||
renderApplicationNotFound(rw, r, api.AccessURL)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to login as they don't have permission to access the app and
|
||||
// they aren't signed in.
|
||||
httpmw.RedirectToLogin(rw, r, httpmw.SignedOutErrorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -67,10 +91,9 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
|||
api.proxyWorkspaceApplication(proxyApplication{
|
||||
Workspace: workspace,
|
||||
Agent: agent,
|
||||
// We do not support port proxying for paths.
|
||||
AppName: chi.URLParam(r, "workspaceapp"),
|
||||
Port: 0,
|
||||
Path: chiPath,
|
||||
App: &app,
|
||||
Port: 0,
|
||||
Path: chiPath,
|
||||
}, rw, r)
|
||||
}
|
||||
|
||||
|
@ -156,16 +179,30 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht
|
|||
workspace := httpmw.WorkspaceParam(r)
|
||||
agent := httpmw.WorkspaceAgentParam(r)
|
||||
|
||||
var workspaceAppPtr *database.WorkspaceApp
|
||||
if app.AppName != "" {
|
||||
workspaceApp, ok := api.lookupWorkspaceApp(rw, r, agent.ID, app.AppName)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
workspaceAppPtr = &workspaceApp
|
||||
}
|
||||
|
||||
// Verify application auth. This function will redirect or
|
||||
// return an error page if the user doesn't have permission.
|
||||
if !api.verifyWorkspaceApplicationAuth(rw, r, workspace, host) {
|
||||
sharingLevel := database.AppSharingLevelOwner
|
||||
if workspaceAppPtr != nil && workspaceAppPtr.SharingLevel != "" {
|
||||
sharingLevel = workspaceAppPtr.SharingLevel
|
||||
}
|
||||
if !api.verifyWorkspaceApplicationSubdomainAuth(rw, r, host, workspace, sharingLevel) {
|
||||
return
|
||||
}
|
||||
|
||||
api.proxyWorkspaceApplication(proxyApplication{
|
||||
Workspace: workspace,
|
||||
Agent: agent,
|
||||
AppName: app.AppName,
|
||||
App: workspaceAppPtr,
|
||||
Port: app.Port,
|
||||
Path: r.URL.Path,
|
||||
}, rw, r)
|
||||
|
@ -231,22 +268,139 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt
|
|||
return app, true
|
||||
}
|
||||
|
||||
// verifyWorkspaceApplicationAuth checks that the request is authorized to
|
||||
// access the given application. If the user does not have a app session key,
|
||||
// lookupWorkspaceApp looks up the workspace application by name in the given
|
||||
// agent and returns it. If the application is not found or there was a server
|
||||
// error while looking it up, an HTML error page is returned and false is
|
||||
// returned so the caller can return early.
|
||||
func (api *API) lookupWorkspaceApp(rw http.ResponseWriter, r *http.Request, agentID uuid.UUID, appName string) (database.WorkspaceApp, bool) {
|
||||
app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{
|
||||
AgentID: agentID,
|
||||
Name: appName,
|
||||
})
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
renderApplicationNotFound(rw, r, api.AccessURL)
|
||||
return database.WorkspaceApp{}, false
|
||||
}
|
||||
if err != nil {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusInternalServerError,
|
||||
Title: "Internal Server Error",
|
||||
Description: "Could not fetch workspace application: " + err.Error(),
|
||||
RetryEnabled: true,
|
||||
DashboardURL: api.AccessURL.String(),
|
||||
})
|
||||
return database.WorkspaceApp{}, false
|
||||
}
|
||||
|
||||
return app, true
|
||||
}
|
||||
|
||||
func (api *API) authorizeWorkspaceApp(r *http.Request, sharingLevel database.AppSharingLevel, workspace database.Workspace) (bool, error) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Short circuit if not authenticated.
|
||||
roles, ok := httpmw.UserAuthorizationOptional(r)
|
||||
if !ok {
|
||||
// The user is not authenticated, so they can only access the app if it
|
||||
// is public.
|
||||
return sharingLevel == database.AppSharingLevelPublic, nil
|
||||
}
|
||||
|
||||
// Do a standard RBAC check. This accounts for share level "owner" and any
|
||||
// other RBAC rules that may be in place.
|
||||
//
|
||||
// Regardless of share level or whether it's enabled or not, the owner of
|
||||
// the workspace can always access applications (as long as their API key's
|
||||
// scope allows it).
|
||||
err := api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), []string{}, rbac.ActionCreate, workspace.ApplicationConnectRBAC())
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
switch sharingLevel {
|
||||
case database.AppSharingLevelOwner:
|
||||
// We essentially already did this above with the regular RBAC check.
|
||||
// Owners can always access their own apps according to RBAC rules, so
|
||||
// they have already been returned from this function.
|
||||
case database.AppSharingLevelAuthenticated:
|
||||
// The user is authenticated at this point, but we need to make sure
|
||||
// that they have ApplicationConnect permissions to their own
|
||||
// workspaces. This ensures that the key's scope has permission to
|
||||
// connect to workspace apps.
|
||||
object := rbac.ResourceWorkspaceApplicationConnect.WithOwner(roles.ID.String())
|
||||
err := api.Authorizer.ByRoleName(ctx, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), []string{}, rbac.ActionCreate, object)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
case database.AppSharingLevelPublic:
|
||||
// We don't really care about scopes and stuff if it's public anyways.
|
||||
// Someone with a restricted-scope API key could just not submit the
|
||||
// API key cookie in the request and access the page.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// No checks were successful.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// fetchWorkspaceApplicationAuth authorizes the user using api.AppAuthorizer
|
||||
// for a given app share level in the given workspace. The user's authorization
|
||||
// status is returned. If a server error occurs, a HTML error page is rendered
|
||||
// and false is returned so the caller can return early.
|
||||
func (api *API) fetchWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, appSharingLevel database.AppSharingLevel) (authed bool, ok bool) {
|
||||
ok, err := api.authorizeWorkspaceApp(r, appSharingLevel, workspace)
|
||||
if err != nil {
|
||||
api.Logger.Error(r.Context(), "authorize workspace app", slog.Error(err))
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusInternalServerError,
|
||||
Title: "Internal Server Error",
|
||||
Description: "Could not verify authorization. Please try again or contact an administrator.",
|
||||
RetryEnabled: true,
|
||||
DashboardURL: api.AccessURL.String(),
|
||||
})
|
||||
return false, false
|
||||
}
|
||||
|
||||
return ok, true
|
||||
}
|
||||
|
||||
// checkWorkspaceApplicationAuth authorizes the user using api.AppAuthorizer
|
||||
// for a given app share level in the given workspace. If the user is not
|
||||
// authorized or a server error occurs, a discrete HTML error page is rendered
|
||||
// and false is returned so the caller can return early.
|
||||
func (api *API) checkWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, appSharingLevel database.AppSharingLevel) bool {
|
||||
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !authed {
|
||||
renderApplicationNotFound(rw, r, api.AccessURL)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// verifyWorkspaceApplicationSubdomainAuth checks that the request is authorized
|
||||
// to access the given application. If the user does not have a app session key,
|
||||
// they will be redirected to the route below. If the user does have a session
|
||||
// key but insufficient permissions a static error page will be rendered.
|
||||
func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, host string) bool {
|
||||
_, ok := httpmw.APIKeyOptional(r)
|
||||
if ok {
|
||||
if !api.Authorize(r, rbac.ActionCreate, workspace.ApplicationConnectRBAC()) {
|
||||
renderApplicationNotFound(rw, r, api.AccessURL)
|
||||
return false
|
||||
}
|
||||
|
||||
// Request should be all good to go!
|
||||
func (api *API) verifyWorkspaceApplicationSubdomainAuth(rw http.ResponseWriter, r *http.Request, host string, workspace database.Workspace, appSharingLevel database.AppSharingLevel) bool {
|
||||
authed, ok := api.fetchWorkspaceApplicationAuth(rw, r, workspace, appSharingLevel)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if authed {
|
||||
return true
|
||||
}
|
||||
|
||||
_, hasAPIKey := httpmw.APIKeyOptional(r)
|
||||
if hasAPIKey {
|
||||
// The request has a valid API key but insufficient permissions.
|
||||
renderApplicationNotFound(rw, r, api.AccessURL)
|
||||
return false
|
||||
}
|
||||
|
||||
// If the request has the special query param then we need to set a cookie
|
||||
// and strip that query parameter.
|
||||
if encryptedAPIKey := r.URL.Query().Get(subdomainProxyAPIKeyParam); encryptedAPIKey != "" {
|
||||
|
@ -421,58 +575,49 @@ type proxyApplication struct {
|
|||
Workspace database.Workspace
|
||||
Agent database.WorkspaceAgent
|
||||
|
||||
// Either AppName or Port must be set, but not both.
|
||||
AppName string
|
||||
Port uint16
|
||||
// Either App or Port must be set, but not both.
|
||||
App *database.WorkspaceApp
|
||||
Port uint16
|
||||
|
||||
// SharingLevel MUST be set to database.AppSharingLevelOwner by default for
|
||||
// ports.
|
||||
SharingLevel database.AppSharingLevel
|
||||
// Path must either be empty or have a leading slash.
|
||||
Path string
|
||||
}
|
||||
|
||||
func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !api.Authorize(r, rbac.ActionCreate, proxyApp.Workspace.ApplicationConnectRBAC()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
|
||||
sharingLevel := database.AppSharingLevelOwner
|
||||
if proxyApp.App != nil && proxyApp.App.SharingLevel != "" {
|
||||
sharingLevel = proxyApp.App.SharingLevel
|
||||
}
|
||||
if !api.checkWorkspaceApplicationAuth(rw, r, proxyApp.Workspace, sharingLevel) {
|
||||
return
|
||||
}
|
||||
|
||||
// If the app does not exist, but the app name is a port number, then
|
||||
// route to the port as an "anonymous app". We only support HTTP for
|
||||
// port-based URLs.
|
||||
//
|
||||
// This is only supported for subdomain-based applications.
|
||||
internalURL := fmt.Sprintf("http://127.0.0.1:%d", proxyApp.Port)
|
||||
|
||||
// If the app name was used instead, fetch the app from the database so we
|
||||
// can get the internal URL.
|
||||
if proxyApp.AppName != "" {
|
||||
app, err := api.Database.GetWorkspaceAppByAgentIDAndName(ctx, database.GetWorkspaceAppByAgentIDAndNameParams{
|
||||
AgentID: proxyApp.Agent.ID,
|
||||
Name: proxyApp.AppName,
|
||||
})
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
renderApplicationNotFound(rw, r, api.AccessURL)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusInternalServerError,
|
||||
Title: "Internal Server Error",
|
||||
Description: "Could not fetch workspace application: " + err.Error(),
|
||||
RetryEnabled: true,
|
||||
DashboardURL: api.AccessURL.String(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !app.Url.Valid {
|
||||
if proxyApp.App != nil {
|
||||
if !proxyApp.App.Url.Valid {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Bad Request",
|
||||
Description: fmt.Sprintf("Application %q does not have a URL set.", app.Name),
|
||||
Description: fmt.Sprintf("Application %q does not have a URL set.", proxyApp.App.Name),
|
||||
RetryEnabled: true,
|
||||
DashboardURL: api.AccessURL.String(),
|
||||
})
|
||||
return
|
||||
}
|
||||
internalURL = app.Url.String
|
||||
internalURL = proxyApp.App.Url.String
|
||||
}
|
||||
|
||||
appURL, err := url.Parse(internalURL)
|
||||
|
@ -692,8 +837,8 @@ func decryptAPIKey(ctx context.Context, db database.Store, encryptedAPIKey strin
|
|||
func renderApplicationNotFound(rw http.ResponseWriter, r *http.Request, accessURL *url.URL) {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusNotFound,
|
||||
Title: "Application not found",
|
||||
Description: "The application or workspace you are trying to access does not exist.",
|
||||
Title: "Application Not Found",
|
||||
Description: "The application or workspace you are trying to access does not exist or you do not have permission to access it.",
|
||||
RetryEnabled: false,
|
||||
DashboardURL: accessURL.String(),
|
||||
})
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -28,11 +29,13 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
proxyTestAgentName = "agent-name"
|
||||
proxyTestAppName = "example"
|
||||
proxyTestAppQuery = "query=true"
|
||||
proxyTestAppBody = "hello world"
|
||||
proxyTestFakeAppName = "fake"
|
||||
proxyTestAgentName = "agent-name"
|
||||
proxyTestAppNameFake = "test-app-fake"
|
||||
proxyTestAppNameOwner = "test-app-owner"
|
||||
proxyTestAppNameAuthenticated = "test-app-authenticated"
|
||||
proxyTestAppNamePublic = "test-app-public"
|
||||
proxyTestAppQuery = "query=true"
|
||||
proxyTestAppBody = "hello world"
|
||||
|
||||
proxyTestSubdomain = "test.coder.com"
|
||||
)
|
||||
|
@ -101,6 +104,8 @@ func setupProxyTest(t *testing.T, workspaceMutators ...func(*codersdk.CreateWork
|
|||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
authToken := uuid.NewString()
|
||||
|
||||
appURL := fmt.Sprintf("http://127.0.0.1:%d?%s", tcpAddr.Port, proxyTestAppQuery)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionDryRun: echo.ProvisionComplete,
|
||||
|
@ -118,13 +123,26 @@ func setupProxyTest(t *testing.T, workspaceMutators ...func(*codersdk.CreateWork
|
|||
},
|
||||
Apps: []*proto.App{
|
||||
{
|
||||
Name: proxyTestAppName,
|
||||
Url: fmt.Sprintf("http://127.0.0.1:%d?%s", tcpAddr.Port, proxyTestAppQuery),
|
||||
}, {
|
||||
Name: proxyTestFakeAppName,
|
||||
Name: proxyTestAppNameFake,
|
||||
SharingLevel: proto.AppSharingLevel_OWNER,
|
||||
// Hopefully this IP and port doesn't exist.
|
||||
Url: "http://127.1.0.1:65535",
|
||||
},
|
||||
{
|
||||
Name: proxyTestAppNameOwner,
|
||||
SharingLevel: proto.AppSharingLevel_OWNER,
|
||||
Url: appURL,
|
||||
},
|
||||
{
|
||||
Name: proxyTestAppNameAuthenticated,
|
||||
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
||||
Url: appURL,
|
||||
},
|
||||
{
|
||||
Name: proxyTestAppNamePublic,
|
||||
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
||||
Url: appURL,
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
|
@ -180,7 +198,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil)
|
||||
resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s", workspace.Name, proxyTestAppNameOwner), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
@ -201,7 +219,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := userClient.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil)
|
||||
resp, err := userClient.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s", workspace.Name, proxyTestAppNameOwner), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
|
@ -213,7 +231,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil)
|
||||
resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s", workspace.Name, proxyTestAppNameOwner), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
@ -225,7 +243,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example/", nil)
|
||||
resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s/", workspace.Name, proxyTestAppNameOwner), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
|
@ -240,7 +258,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example/?"+proxyTestAppQuery, nil)
|
||||
resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s/?%s", workspace.Name, proxyTestAppNameOwner, proxyTestAppQuery), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
@ -255,7 +273,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/fake/", nil)
|
||||
resp, err := client.Request(ctx, http.MethodGet, fmt.Sprintf("/@me/%s/apps/%s/", workspace.Name, proxyTestAppNameFake), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusBadGateway, resp.StatusCode)
|
||||
|
@ -281,7 +299,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Try to load the application without authentication.
|
||||
subdomain := fmt.Sprintf("%s--%s--%s--%s", proxyTestAppName, proxyTestAgentName, workspace.Name, user.Username)
|
||||
subdomain := fmt.Sprintf("%s--%s--%s--%s", proxyTestAppNameOwner, proxyTestAgentName, workspace.Name, user.Username)
|
||||
u, err := url.Parse(fmt.Sprintf("http://%s.%s/test", subdomain, proxyTestSubdomain))
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
|
@ -607,7 +625,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := userClient.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppName), nil)
|
||||
resp, err := userClient.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppNameOwner), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
|
@ -619,7 +637,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
slashlessURL := proxyURL(t, proxyTestAppName, "")
|
||||
slashlessURL := proxyURL(t, proxyTestAppNameOwner, "")
|
||||
resp, err := client.Request(ctx, http.MethodGet, slashlessURL, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
@ -636,7 +654,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
querylessURL := proxyURL(t, proxyTestAppName, "/", "")
|
||||
querylessURL := proxyURL(t, proxyTestAppNameOwner, "/", "")
|
||||
resp, err := client.Request(ctx, http.MethodGet, querylessURL, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
@ -653,7 +671,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppName, "/", proxyTestAppQuery), nil)
|
||||
resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppNameOwner, "/", proxyTestAppQuery), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
@ -683,7 +701,7 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestFakeAppName, "/", ""), nil)
|
||||
resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppNameFake, "/", ""), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusBadGateway, resp.StatusCode)
|
||||
|
@ -708,3 +726,168 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
|
|||
require.Contains(t, resBody.Message, "Coder reserves ports less than")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppSharing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
setup := func(t *testing.T) (workspace codersdk.Workspace, agnt codersdk.WorkspaceAgent, user codersdk.User, client *codersdk.Client, clientInOtherOrg *codersdk.Client, clientWithNoAuth *codersdk.Client) {
|
||||
//nolint:gosec
|
||||
const password = "password"
|
||||
|
||||
client, _, workspace, _ = setupProxyTest(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
user, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that the apps have the correct sharing levels set.
|
||||
workspaceBuild, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
agnt = workspaceBuild.Resources[0].Agents[0]
|
||||
found := map[string]codersdk.WorkspaceAppSharingLevel{}
|
||||
expected := map[string]codersdk.WorkspaceAppSharingLevel{
|
||||
proxyTestAppNameFake: codersdk.WorkspaceAppSharingLevelOwner,
|
||||
proxyTestAppNameOwner: codersdk.WorkspaceAppSharingLevelOwner,
|
||||
proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated,
|
||||
proxyTestAppNamePublic: codersdk.WorkspaceAppSharingLevelPublic,
|
||||
}
|
||||
for _, app := range agnt.Apps {
|
||||
found[app.Name] = app.SharingLevel
|
||||
}
|
||||
require.Equal(t, expected, found, "apps have incorrect sharing levels")
|
||||
|
||||
// Create a user in a different org.
|
||||
otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
|
||||
Name: "a-different-org",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
userInOtherOrg, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
|
||||
Email: "no-template-access@coder.com",
|
||||
Username: "no-template-access",
|
||||
Password: password,
|
||||
OrganizationID: otherOrg.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
clientInOtherOrg = codersdk.New(client.URL)
|
||||
loginRes, err := clientInOtherOrg.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
|
||||
Email: userInOtherOrg.Email,
|
||||
Password: password,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
clientInOtherOrg.SessionToken = loginRes.SessionToken
|
||||
clientInOtherOrg.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
// Create an unauthenticated codersdk client.
|
||||
clientWithNoAuth = codersdk.New(client.URL)
|
||||
clientWithNoAuth.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
return workspace, agnt, user, client, clientInOtherOrg, clientWithNoAuth
|
||||
}
|
||||
|
||||
verifyAccess := func(t *testing.T, username, workspaceName, agentName, appName string, client *codersdk.Client, shouldHaveAccess, shouldRedirectToLogin bool) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// If the client has a session token, we also want to check that a
|
||||
// scoped key works.
|
||||
clients := []*codersdk.Client{client}
|
||||
if client.SessionToken != "" {
|
||||
token, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
|
||||
Scope: codersdk.APIKeyScopeApplicationConnect,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
scopedClient := codersdk.New(client.URL)
|
||||
scopedClient.SessionToken = token.Key
|
||||
scopedClient.HTTPClient.CheckRedirect = client.HTTPClient.CheckRedirect
|
||||
|
||||
clients = append(clients, scopedClient)
|
||||
}
|
||||
|
||||
for i, client := range clients {
|
||||
msg := fmt.Sprintf("client %d", i)
|
||||
|
||||
appPath := fmt.Sprintf("/@%s/%s.%s/apps/%s/?%s", username, workspaceName, agentName, appName, proxyTestAppQuery)
|
||||
res, err := client.Request(ctx, http.MethodGet, appPath, nil)
|
||||
require.NoError(t, err, msg)
|
||||
|
||||
dump, err := httputil.DumpResponse(res, true)
|
||||
res.Body.Close()
|
||||
require.NoError(t, err, msg)
|
||||
t.Logf("response dump: %s", dump)
|
||||
|
||||
if !shouldHaveAccess {
|
||||
if shouldRedirectToLogin {
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, res.StatusCode, "should not have access, expected temporary redirect. "+msg)
|
||||
location, err := res.Location()
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, "/login", location.Path, "should not have access, expected redirect to /login. "+msg)
|
||||
} else {
|
||||
// If the user doesn't have access we return 404 to avoid
|
||||
// leaking information about the existence of the app.
|
||||
assert.Equal(t, http.StatusNotFound, res.StatusCode, "should not have access, expected not found. "+msg)
|
||||
}
|
||||
}
|
||||
|
||||
if shouldHaveAccess {
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "should have access, expected ok. "+msg)
|
||||
assert.Contains(t, string(dump), "hello world", "should have access, expected hello world. "+msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("Level", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
workspace, agent, user, client, clientInOtherOrg, clientWithNoAuth := setup(t)
|
||||
|
||||
t.Run("Owner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Owner should be able to access their own workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, client, true, false)
|
||||
|
||||
// Authenticated users should not have access to a workspace that
|
||||
// they do not own.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientInOtherOrg, false, false)
|
||||
|
||||
// Unauthenticated user should not have any access.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameOwner, clientWithNoAuth, false, true)
|
||||
})
|
||||
|
||||
t.Run("Authenticated", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Owner should be able to access their own workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, client, true, false)
|
||||
|
||||
// Authenticated users should be able to access the workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientInOtherOrg, true, false)
|
||||
|
||||
// Unauthenticated user should not have any access.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNameAuthenticated, clientWithNoAuth, false, true)
|
||||
})
|
||||
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Owner should be able to access their own workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, client, true, false)
|
||||
|
||||
// Authenticated users should be able to access the workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientInOtherOrg, true, false)
|
||||
|
||||
// Unauthenticated user should be able to access the workspace.
|
||||
verifyAccess(t, user.Username, workspace.Name, agent.Name, proxyTestAppNamePublic, clientWithNoAuth, true, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,13 +13,14 @@ import (
|
|||
type APIKey struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
// NOTE: do not ever return the HashedSecret
|
||||
UserID uuid.UUID `json:"user_id" validate:"required"`
|
||||
LastUsed time.Time `json:"last_used" validate:"required"`
|
||||
ExpiresAt time.Time `json:"expires_at" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||
LoginType LoginType `json:"login_type" validate:"required"`
|
||||
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
||||
UserID uuid.UUID `json:"user_id" validate:"required"`
|
||||
LastUsed time.Time `json:"last_used" validate:"required"`
|
||||
ExpiresAt time.Time `json:"expires_at" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||
LoginType LoginType `json:"login_type" validate:"required"`
|
||||
Scope APIKeyScope `json:"scope" validate:"required"`
|
||||
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
||||
}
|
||||
|
||||
type LoginType string
|
||||
|
@ -31,32 +32,51 @@ const (
|
|||
LoginTypeToken LoginType = "token"
|
||||
)
|
||||
|
||||
type APIKeyScope string
|
||||
|
||||
const (
|
||||
APIKeyScopeAll APIKeyScope = "all"
|
||||
APIKeyScopeApplicationConnect APIKeyScope = "application_connect"
|
||||
)
|
||||
|
||||
type CreateTokenRequest struct {
|
||||
Scope APIKeyScope `json:"scope"`
|
||||
}
|
||||
|
||||
// GenerateAPIKeyResponse contains an API key for a user.
|
||||
type GenerateAPIKeyResponse struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// CreateToken generates an API key that doesn't expire.
|
||||
func (c *Client) CreateToken(ctx context.Context, userID string) (*GenerateAPIKeyResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil)
|
||||
func (c *Client) CreateToken(ctx context.Context, userID string, req CreateTokenRequest) (GenerateAPIKeyResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return GenerateAPIKeyResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusCreated {
|
||||
return nil, readBodyAsError(res)
|
||||
return GenerateAPIKeyResponse{}, readBodyAsError(res)
|
||||
}
|
||||
apiKey := &GenerateAPIKeyResponse{}
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
||||
|
||||
var apiKey GenerateAPIKeyResponse
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
||||
}
|
||||
|
||||
// CreateAPIKey generates an API key for the user ID provided.
|
||||
func (c *Client) CreateAPIKey(ctx context.Context, user string) (*GenerateAPIKeyResponse, error) {
|
||||
// DEPRECATED: use CreateToken instead.
|
||||
func (c *Client) CreateAPIKey(ctx context.Context, user string) (GenerateAPIKeyResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return GenerateAPIKeyResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusCreated {
|
||||
return nil, readBodyAsError(res)
|
||||
return GenerateAPIKeyResponse{}, readBodyAsError(res)
|
||||
}
|
||||
apiKey := &GenerateAPIKeyResponse{}
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
||||
|
||||
var apiKey GenerateAPIKeyResponse
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
||||
}
|
||||
|
||||
// GetTokens list machine API keys.
|
||||
|
|
|
@ -96,11 +96,6 @@ type LoginWithPasswordResponse struct {
|
|||
SessionToken string `json:"session_token" validate:"required"`
|
||||
}
|
||||
|
||||
// GenerateAPIKeyResponse contains an API key for a user.
|
||||
type GenerateAPIKeyResponse struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type CreateOrganizationRequest struct {
|
||||
Name string `json:"name" validate:"required,username"`
|
||||
}
|
||||
|
|
|
@ -13,6 +13,14 @@ const (
|
|||
WorkspaceAppHealthUnhealthy WorkspaceAppHealth = "unhealthy"
|
||||
)
|
||||
|
||||
type WorkspaceAppSharingLevel string
|
||||
|
||||
const (
|
||||
WorkspaceAppSharingLevelOwner WorkspaceAppSharingLevel = "owner"
|
||||
WorkspaceAppSharingLevelAuthenticated WorkspaceAppSharingLevel = "authenticated"
|
||||
WorkspaceAppSharingLevelPublic WorkspaceAppSharingLevel = "public"
|
||||
)
|
||||
|
||||
type WorkspaceApp struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
// Name is a unique identifier attached to an agent.
|
||||
|
@ -25,7 +33,8 @@ type WorkspaceApp struct {
|
|||
// `coder server` or via a hostname-based dev URL. If this is set to true
|
||||
// and there is no app wildcard configured on the server, the app will not
|
||||
// be accessible in the UI.
|
||||
Subdomain bool `json:"subdomain"`
|
||||
Subdomain bool `json:"subdomain"`
|
||||
SharingLevel WorkspaceAppSharingLevel `json:"sharing_level"`
|
||||
// Healthcheck specifies the configuration for checking app health.
|
||||
Healthcheck Healthcheck `json:"healthcheck"`
|
||||
Health WorkspaceAppHealth `json:"health"`
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
|
@ -38,10 +38,12 @@ resource "coder_agent" "dev" {
|
|||
}
|
||||
|
||||
resource "coder_app" "code-server" {
|
||||
agent_id = coder_agent.dev.id
|
||||
name = "code-server"
|
||||
url = "http://localhost:13337/"
|
||||
icon = "/icon/code.svg"
|
||||
agent_id = coder_agent.dev.id
|
||||
name = "code-server"
|
||||
url = "http://localhost:13337/"
|
||||
icon = "/icon/code.svg"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
|
@ -50,7 +52,6 @@ resource "coder_app" "code-server" {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
resource "docker_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-home"
|
||||
}
|
||||
|
|
|
@ -35,6 +35,12 @@ func New(ctx context.Context, options *Options) (*API, error) {
|
|||
if options.Keys == nil {
|
||||
options.Keys = Keys
|
||||
}
|
||||
if options.Options == nil {
|
||||
options.Options = &coderd.Options{}
|
||||
}
|
||||
if options.Options.Authorizer == nil {
|
||||
options.Options.Authorizer = rbac.NewAuthorizer()
|
||||
}
|
||||
ctx, cancelFunc := context.WithCancel(ctx)
|
||||
api := &API{
|
||||
AGPL: coderd.New(options.Options),
|
||||
|
@ -92,7 +98,7 @@ func New(ctx context.Context, options *Options) (*API, error) {
|
|||
r.Route("/workspace-quota", func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractUserParam(options.Database))
|
||||
r.Use(httpmw.ExtractUserParam(options.Database, false))
|
||||
r.Get("/", api.workspaceQuota)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -118,19 +118,19 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
|
|||
if options.GraceAt.IsZero() {
|
||||
options.GraceAt = time.Now().Add(time.Hour)
|
||||
}
|
||||
auditLog := int64(0)
|
||||
var auditLog int64
|
||||
if options.AuditLog {
|
||||
auditLog = 1
|
||||
}
|
||||
browserOnly := int64(0)
|
||||
var browserOnly int64
|
||||
if options.BrowserOnly {
|
||||
browserOnly = 1
|
||||
}
|
||||
scim := int64(0)
|
||||
var scim int64
|
||||
if options.SCIM {
|
||||
scim = 1
|
||||
}
|
||||
workspaceQuota := int64(0)
|
||||
var workspaceQuota int64
|
||||
if options.WorkspaceQuota {
|
||||
workspaceQuota = 1
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package coderd_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
|
@ -16,6 +17,14 @@ import (
|
|||
"github.com/coder/coder/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
// App names for each app sharing level.
|
||||
const (
|
||||
testAppNameOwner = "test-app-owner"
|
||||
testAppNameAuthenticated = "test-app-authenticated"
|
||||
testAppNamePublic = "test-app-public"
|
||||
)
|
||||
|
||||
func TestBlockNonBrowser(t *testing.T) {
|
||||
|
@ -32,8 +41,8 @@ func TestBlockNonBrowser(t *testing.T) {
|
|||
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
|
||||
BrowserOnly: true,
|
||||
})
|
||||
id := setupWorkspaceAgent(t, client, user)
|
||||
_, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, id)
|
||||
_, agent := setupWorkspaceAgent(t, client, user, 0)
|
||||
_, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, agent.ID)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||
|
@ -49,14 +58,14 @@ func TestBlockNonBrowser(t *testing.T) {
|
|||
coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
|
||||
BrowserOnly: false,
|
||||
})
|
||||
id := setupWorkspaceAgent(t, client, user)
|
||||
conn, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, id)
|
||||
_, agent := setupWorkspaceAgent(t, client, user, 0)
|
||||
conn, err := client.DialWorkspaceAgentTailnet(context.Background(), slog.Logger{}, agent.ID)
|
||||
require.NoError(t, err)
|
||||
_ = conn.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse) uuid.UUID {
|
||||
func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.CreateFirstUserResponse, appPort uint16) (codersdk.Workspace, codersdk.WorkspaceAgent) {
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
|
@ -72,6 +81,23 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr
|
|||
Auth: &proto.Agent_Token{
|
||||
Token: authToken,
|
||||
},
|
||||
Apps: []*proto.App{
|
||||
{
|
||||
Name: testAppNameOwner,
|
||||
SharingLevel: proto.AppSharingLevel_OWNER,
|
||||
Url: fmt.Sprintf("http://localhost:%d", appPort),
|
||||
},
|
||||
{
|
||||
Name: testAppNameAuthenticated,
|
||||
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
||||
Url: fmt.Sprintf("http://localhost:%d", appPort),
|
||||
},
|
||||
{
|
||||
Name: testAppNamePublic,
|
||||
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
||||
Url: fmt.Sprintf("http://localhost:%d", appPort),
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
|
@ -89,9 +115,16 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr
|
|||
CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet,
|
||||
Logger: slogtest.Make(t, nil).Named("agent"),
|
||||
})
|
||||
defer func() {
|
||||
t.Cleanup(func() {
|
||||
_ = agentCloser.Close()
|
||||
}()
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
||||
return resources[0].Agents[0].ID
|
||||
agnt, err := client.WorkspaceAgent(ctx, resources[0].Agents[0].ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return workspace, agnt
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ terraform {
|
|||
}
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,6 +110,7 @@ resource "coder_app" "code-server" {
|
|||
icon = "/icon/code.svg"
|
||||
url = "http://localhost:13337?folder=/home/coder"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,10 +86,12 @@ resource "coder_agent" "main" {
|
|||
}
|
||||
|
||||
resource "coder_app" "code-server" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "code-server"
|
||||
url = "http://localhost:13337/?folder=/home/coder"
|
||||
icon = "/icon/code.svg"
|
||||
agent_id = coder_agent.main.id
|
||||
name = "code-server"
|
||||
url = "http://localhost:13337/?folder=/home/coder"
|
||||
icon = "/icon/code.svg"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
azurerm = {
|
||||
source = "hashicorp/azurerm"
|
||||
|
|
|
@ -43,10 +43,12 @@ resource "null_resource" "fake-disk" {
|
|||
|
||||
resource "coder_app" "fake-app" {
|
||||
# Access :8080 in the workspace from the Coder dashboard.
|
||||
name = "VS Code"
|
||||
icon = "/icon/code.svg"
|
||||
agent_id = "fake-compute"
|
||||
url = "http://localhost:8080"
|
||||
name = "VS Code"
|
||||
icon = "/icon/code.svg"
|
||||
agent_id = "fake-compute"
|
||||
url = "http://localhost:8080"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:8080/healthz"
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
digitalocean = {
|
||||
source = "digitalocean/digitalocean"
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
|
@ -38,9 +38,12 @@ resource "coder_agent" "main" {
|
|||
}
|
||||
|
||||
resource "coder_app" "code-server" {
|
||||
agent_id = coder_agent.main.id
|
||||
url = "http://localhost:8080/?folder=/home/coder"
|
||||
icon = "/icon/code.svg"
|
||||
agent_id = coder_agent.main.id
|
||||
name = "code-server"
|
||||
url = "http://localhost:8080/?folder=/home/coder"
|
||||
icon = "/icon/code.svg"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:8080/healthz"
|
||||
|
|
|
@ -3,7 +3,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
|
@ -34,10 +34,12 @@ resource "coder_agent" "main" {
|
|||
}
|
||||
|
||||
resource "coder_app" "code-server" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "code-server"
|
||||
url = "http://localhost:13337/?folder=/home/coder"
|
||||
icon = "/icon/code.svg"
|
||||
agent_id = coder_agent.main.id
|
||||
name = "code-server"
|
||||
url = "http://localhost:13337/?folder=/home/coder"
|
||||
icon = "/icon/code.svg"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
|
|
|
@ -9,7 +9,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
|
@ -43,10 +43,13 @@ resource "coder_agent" "main" {
|
|||
}
|
||||
|
||||
resource "coder_app" "code-server" {
|
||||
agent_id = coder_agent.main.id
|
||||
name = "code-server"
|
||||
url = "http://localhost:13337/?folder=/home/coder"
|
||||
icon = "/icon/code.svg"
|
||||
agent_id = coder_agent.main.id
|
||||
name = "code-server"
|
||||
url = "http://localhost:13337/?folder=/home/coder"
|
||||
icon = "/icon/code.svg"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
interval = 5
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
|
@ -65,6 +65,7 @@ resource "coder_app" "code-server" {
|
|||
icon = "/icon/code.svg"
|
||||
url = "http://localhost:13337?folder=/home/coder"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
|
@ -55,6 +55,7 @@ resource "coder_app" "code-server" {
|
|||
icon = "/icon/code.svg"
|
||||
url = "http://localhost:13337?folder=/home/coder"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
|
@ -76,6 +76,7 @@ resource "coder_app" "code-server" {
|
|||
icon = "/icon/code.svg"
|
||||
url = "http://localhost:13337?folder=/home/coder"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
|
|
|
@ -30,6 +30,7 @@ type agentAppAttributes struct {
|
|||
Icon string `mapstructure:"icon"`
|
||||
URL string `mapstructure:"url"`
|
||||
Command string `mapstructure:"command"`
|
||||
Share string `mapstructure:"share"`
|
||||
Subdomain bool `mapstructure:"subdomain"`
|
||||
Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"`
|
||||
}
|
||||
|
@ -235,6 +236,16 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
|
|||
}
|
||||
}
|
||||
|
||||
sharingLevel := proto.AppSharingLevel_OWNER
|
||||
switch strings.ToLower(attrs.Share) {
|
||||
case "owner":
|
||||
sharingLevel = proto.AppSharingLevel_OWNER
|
||||
case "authenticated":
|
||||
sharingLevel = proto.AppSharingLevel_AUTHENTICATED
|
||||
case "public":
|
||||
sharingLevel = proto.AppSharingLevel_PUBLIC
|
||||
}
|
||||
|
||||
for _, agents := range resourceAgents {
|
||||
for _, agent := range agents {
|
||||
// Find agents with the matching ID and associate them!
|
||||
|
@ -242,12 +253,13 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
|
|||
continue
|
||||
}
|
||||
agent.Apps = append(agent.Apps, &proto.App{
|
||||
Name: attrs.Name,
|
||||
Command: attrs.Command,
|
||||
Url: attrs.URL,
|
||||
Icon: attrs.Icon,
|
||||
Subdomain: attrs.Subdomain,
|
||||
Healthcheck: healthcheck,
|
||||
Name: attrs.Name,
|
||||
Command: attrs.Command,
|
||||
Url: attrs.URL,
|
||||
Icon: attrs.Icon,
|
||||
Subdomain: attrs.Subdomain,
|
||||
SharingLevel: sharingLevel,
|
||||
Healthcheck: healthcheck,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "0.5.0"
|
||||
version = "0.5.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,55 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) {
|
|||
return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type AppSharingLevel int32
|
||||
|
||||
const (
|
||||
AppSharingLevel_OWNER AppSharingLevel = 0
|
||||
AppSharingLevel_AUTHENTICATED AppSharingLevel = 1
|
||||
AppSharingLevel_PUBLIC AppSharingLevel = 2
|
||||
)
|
||||
|
||||
// Enum value maps for AppSharingLevel.
|
||||
var (
|
||||
AppSharingLevel_name = map[int32]string{
|
||||
0: "OWNER",
|
||||
1: "AUTHENTICATED",
|
||||
2: "PUBLIC",
|
||||
}
|
||||
AppSharingLevel_value = map[string]int32{
|
||||
"OWNER": 0,
|
||||
"AUTHENTICATED": 1,
|
||||
"PUBLIC": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x AppSharingLevel) Enum() *AppSharingLevel {
|
||||
p := new(AppSharingLevel)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x AppSharingLevel) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (AppSharingLevel) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_provisionersdk_proto_provisioner_proto_enumTypes[1].Descriptor()
|
||||
}
|
||||
|
||||
func (AppSharingLevel) Type() protoreflect.EnumType {
|
||||
return &file_provisionersdk_proto_provisioner_proto_enumTypes[1]
|
||||
}
|
||||
|
||||
func (x AppSharingLevel) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AppSharingLevel.Descriptor instead.
|
||||
func (AppSharingLevel) EnumDescriptor() ([]byte, []int) {
|
||||
return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
type WorkspaceTransition int32
|
||||
|
||||
const (
|
||||
|
@ -109,11 +158,11 @@ func (x WorkspaceTransition) String() string {
|
|||
}
|
||||
|
||||
func (WorkspaceTransition) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_provisionersdk_proto_provisioner_proto_enumTypes[1].Descriptor()
|
||||
return file_provisionersdk_proto_provisioner_proto_enumTypes[2].Descriptor()
|
||||
}
|
||||
|
||||
func (WorkspaceTransition) Type() protoreflect.EnumType {
|
||||
return &file_provisionersdk_proto_provisioner_proto_enumTypes[1]
|
||||
return &file_provisionersdk_proto_provisioner_proto_enumTypes[2]
|
||||
}
|
||||
|
||||
func (x WorkspaceTransition) Number() protoreflect.EnumNumber {
|
||||
|
@ -122,7 +171,7 @@ func (x WorkspaceTransition) Number() protoreflect.EnumNumber {
|
|||
|
||||
// Deprecated: Use WorkspaceTransition.Descriptor instead.
|
||||
func (WorkspaceTransition) EnumDescriptor() ([]byte, []int) {
|
||||
return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{1}
|
||||
return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
type ParameterSource_Scheme int32
|
||||
|
@ -152,11 +201,11 @@ func (x ParameterSource_Scheme) String() string {
|
|||
}
|
||||
|
||||
func (ParameterSource_Scheme) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_provisionersdk_proto_provisioner_proto_enumTypes[2].Descriptor()
|
||||
return file_provisionersdk_proto_provisioner_proto_enumTypes[3].Descriptor()
|
||||
}
|
||||
|
||||
func (ParameterSource_Scheme) Type() protoreflect.EnumType {
|
||||
return &file_provisionersdk_proto_provisioner_proto_enumTypes[2]
|
||||
return &file_provisionersdk_proto_provisioner_proto_enumTypes[3]
|
||||
}
|
||||
|
||||
func (x ParameterSource_Scheme) Number() protoreflect.EnumNumber {
|
||||
|
@ -198,11 +247,11 @@ func (x ParameterDestination_Scheme) String() string {
|
|||
}
|
||||
|
||||
func (ParameterDestination_Scheme) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_provisionersdk_proto_provisioner_proto_enumTypes[3].Descriptor()
|
||||
return file_provisionersdk_proto_provisioner_proto_enumTypes[4].Descriptor()
|
||||
}
|
||||
|
||||
func (ParameterDestination_Scheme) Type() protoreflect.EnumType {
|
||||
return &file_provisionersdk_proto_provisioner_proto_enumTypes[3]
|
||||
return &file_provisionersdk_proto_provisioner_proto_enumTypes[4]
|
||||
}
|
||||
|
||||
func (x ParameterDestination_Scheme) Number() protoreflect.EnumNumber {
|
||||
|
@ -244,11 +293,11 @@ func (x ParameterSchema_TypeSystem) String() string {
|
|||
}
|
||||
|
||||
func (ParameterSchema_TypeSystem) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_provisionersdk_proto_provisioner_proto_enumTypes[4].Descriptor()
|
||||
return file_provisionersdk_proto_provisioner_proto_enumTypes[5].Descriptor()
|
||||
}
|
||||
|
||||
func (ParameterSchema_TypeSystem) Type() protoreflect.EnumType {
|
||||
return &file_provisionersdk_proto_provisioner_proto_enumTypes[4]
|
||||
return &file_provisionersdk_proto_provisioner_proto_enumTypes[5]
|
||||
}
|
||||
|
||||
func (x ParameterSchema_TypeSystem) Number() protoreflect.EnumNumber {
|
||||
|
@ -850,12 +899,13 @@ type App struct {
|
|||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"`
|
||||
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||
Subdomain bool `protobuf:"varint,5,opt,name=subdomain,proto3" json:"subdomain,omitempty"`
|
||||
Healthcheck *Healthcheck `protobuf:"bytes,6,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Command string `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"`
|
||||
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
|
||||
Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||
Subdomain bool `protobuf:"varint,5,opt,name=subdomain,proto3" json:"subdomain,omitempty"`
|
||||
Healthcheck *Healthcheck `protobuf:"bytes,6,opt,name=healthcheck,proto3" json:"healthcheck,omitempty"`
|
||||
SharingLevel AppSharingLevel `protobuf:"varint,7,opt,name=sharing_level,json=sharingLevel,proto3,enum=provisioner.AppSharingLevel" json:"sharing_level,omitempty"`
|
||||
}
|
||||
|
||||
func (x *App) Reset() {
|
||||
|
@ -932,6 +982,13 @@ func (x *App) GetHealthcheck() *Healthcheck {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *App) GetSharingLevel() AppSharingLevel {
|
||||
if x != nil {
|
||||
return x.SharingLevel
|
||||
}
|
||||
return AppSharingLevel_OWNER
|
||||
}
|
||||
|
||||
// Healthcheck represents configuration for checking for app readiness.
|
||||
type Healthcheck struct {
|
||||
state protoimpl.MessageState
|
||||
|
@ -1952,7 +2009,7 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{
|
|||
0x0a, 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
|
||||
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0xb3,
|
||||
0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0xf6,
|
||||
0x01, 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f,
|
||||
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d,
|
||||
|
@ -1964,110 +2021,118 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{
|
|||
0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x48, 0x65, 0x61, 0x6c,
|
||||
0x74, 0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63,
|
||||
0x68, 0x65, 0x63, 0x6b, 0x22, 0x59, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x63, 0x68,
|
||||
0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61,
|
||||
0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61,
|
||||
0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x22,
|
||||
0xad, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03,
|
||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73,
|
||||
0x12, 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03,
|
||||
0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
|
||||
0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
|
||||
0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x68, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, 0x69, 0x64, 0x65,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x69, 0x63, 0x6f, 0x6e, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
|
||||
0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73,
|
||||
0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e,
|
||||
0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c,
|
||||
0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22,
|
||||
0xfc, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72,
|
||||
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f,
|
||||
0x72, 0x79, 0x1a, 0x55, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49,
|
||||
0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65,
|
||||
0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76,
|
||||
0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65,
|
||||
0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74,
|
||||
0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73,
|
||||
0x68, 0x65, 0x63, 0x6b, 0x12, 0x41, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x5f,
|
||||
0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61,
|
||||
0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x0c, 0x73, 0x68, 0x61, 0x72, 0x69,
|
||||
0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x59, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x6c, 0x74,
|
||||
0x68, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65,
|
||||
0x72, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65,
|
||||
0x72, 0x76, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c,
|
||||
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f,
|
||||
0x6c, 0x64, 0x22, 0xad, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74,
|
||||
0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65,
|
||||
0x6e, 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18,
|
||||
0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74,
|
||||
0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x68, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68,
|
||||
0x69, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64,
|
||||
0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73,
|
||||
0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09,
|
||||
0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f,
|
||||
0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75,
|
||||
0x6c, 0x6c, 0x22, 0xfc, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63,
|
||||
0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65,
|
||||
0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x55, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74,
|
||||
0x65, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73,
|
||||
0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d,
|
||||
0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61,
|
||||
0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39,
|
||||
0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50,
|
||||
0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52,
|
||||
0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70,
|
||||
0x65, 0x22, 0xae, 0x07, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a,
|
||||
0xd1, 0x02, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09,
|
||||
0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72,
|
||||
0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54,
|
||||
0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73,
|
||||
0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25,
|
||||
0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63,
|
||||
0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61,
|
||||
0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e,
|
||||
0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21,
|
||||
0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49,
|
||||
0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f,
|
||||
0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77,
|
||||
0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12,
|
||||
0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e,
|
||||
0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13,
|
||||
0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d,
|
||||
0x61, 0x69, 0x6c, 0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x1c, 0x0a,
|
||||
0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x46, 0x0a, 0x10, 0x70,
|
||||
0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18,
|
||||
0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18,
|
||||
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65,
|
||||
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
|
||||
0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||
0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75,
|
||||
0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a,
|
||||
0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, 0x01, 0x0a, 0x07, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61,
|
||||
0x72, 0x74, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x37, 0x0a, 0x06, 0x63,
|
||||
0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61,
|
||||
0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x1a, 0x6b, 0x0a, 0x08,
|
||||
0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74,
|
||||
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14,
|
||||
0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
|
||||
0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09,
|
||||
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, 0x0a, 0x08, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
|
||||
0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63,
|
||||
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73,
|
||||
0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f,
|
||||
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xae,
|
||||
0x07, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xd1, 0x02, 0x0a,
|
||||
0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64,
|
||||
0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f,
|
||||
0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70,
|
||||
0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e,
|
||||
0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63,
|
||||
0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77,
|
||||
0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61,
|
||||
0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f,
|
||||
0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72,
|
||||
0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77,
|
||||
0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c,
|
||||
0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b,
|
||||
0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15,
|
||||
0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f,
|
||||
0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72,
|
||||
0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c,
|
||||
0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69,
|
||||
0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64,
|
||||
0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61,
|
||||
0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03,
|
||||
0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
|
||||
0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52,
|
||||
0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73,
|
||||
0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
|
||||
0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64,
|
||||
0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a,
|
||||
0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74,
|
||||
0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05,
|
||||
0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x08, 0x0a, 0x06,
|
||||
0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e,
|
||||
0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48,
|
||||
0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63,
|
||||
0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65,
|
||||
0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x1a, 0x6b, 0x0a, 0x08, 0x43, 0x6f, 0x6d,
|
||||
0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65,
|
||||
0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
|
||||
0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03,
|
||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f,
|
||||
0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70,
|
||||
0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63,
|
||||
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a,
|
||||
0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54,
|
||||
0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10,
|
||||
0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57,
|
||||
0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04,
|
||||
0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x3d, 0x0a, 0x08, 0x63,
|
||||
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76,
|
||||
0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00,
|
||||
0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79,
|
||||
0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09,
|
||||
0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42,
|
||||
0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08,
|
||||
0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f,
|
||||
0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e,
|
||||
0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10,
|
||||
0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54,
|
||||
0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02,
|
||||
0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61,
|
||||
0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54,
|
||||
0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07,
|
||||
|
@ -2100,72 +2165,74 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte {
|
|||
return file_provisionersdk_proto_provisioner_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
|
||||
var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 6)
|
||||
var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
|
||||
var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{
|
||||
(LogLevel)(0), // 0: provisioner.LogLevel
|
||||
(WorkspaceTransition)(0), // 1: provisioner.WorkspaceTransition
|
||||
(ParameterSource_Scheme)(0), // 2: provisioner.ParameterSource.Scheme
|
||||
(ParameterDestination_Scheme)(0), // 3: provisioner.ParameterDestination.Scheme
|
||||
(ParameterSchema_TypeSystem)(0), // 4: provisioner.ParameterSchema.TypeSystem
|
||||
(*Empty)(nil), // 5: provisioner.Empty
|
||||
(*ParameterSource)(nil), // 6: provisioner.ParameterSource
|
||||
(*ParameterDestination)(nil), // 7: provisioner.ParameterDestination
|
||||
(*ParameterValue)(nil), // 8: provisioner.ParameterValue
|
||||
(*ParameterSchema)(nil), // 9: provisioner.ParameterSchema
|
||||
(*Log)(nil), // 10: provisioner.Log
|
||||
(*InstanceIdentityAuth)(nil), // 11: provisioner.InstanceIdentityAuth
|
||||
(*Agent)(nil), // 12: provisioner.Agent
|
||||
(*App)(nil), // 13: provisioner.App
|
||||
(*Healthcheck)(nil), // 14: provisioner.Healthcheck
|
||||
(*Resource)(nil), // 15: provisioner.Resource
|
||||
(*Parse)(nil), // 16: provisioner.Parse
|
||||
(*Provision)(nil), // 17: provisioner.Provision
|
||||
nil, // 18: provisioner.Agent.EnvEntry
|
||||
(*Resource_Metadata)(nil), // 19: provisioner.Resource.Metadata
|
||||
(*Parse_Request)(nil), // 20: provisioner.Parse.Request
|
||||
(*Parse_Complete)(nil), // 21: provisioner.Parse.Complete
|
||||
(*Parse_Response)(nil), // 22: provisioner.Parse.Response
|
||||
(*Provision_Metadata)(nil), // 23: provisioner.Provision.Metadata
|
||||
(*Provision_Start)(nil), // 24: provisioner.Provision.Start
|
||||
(*Provision_Cancel)(nil), // 25: provisioner.Provision.Cancel
|
||||
(*Provision_Request)(nil), // 26: provisioner.Provision.Request
|
||||
(*Provision_Complete)(nil), // 27: provisioner.Provision.Complete
|
||||
(*Provision_Response)(nil), // 28: provisioner.Provision.Response
|
||||
(AppSharingLevel)(0), // 1: provisioner.AppSharingLevel
|
||||
(WorkspaceTransition)(0), // 2: provisioner.WorkspaceTransition
|
||||
(ParameterSource_Scheme)(0), // 3: provisioner.ParameterSource.Scheme
|
||||
(ParameterDestination_Scheme)(0), // 4: provisioner.ParameterDestination.Scheme
|
||||
(ParameterSchema_TypeSystem)(0), // 5: provisioner.ParameterSchema.TypeSystem
|
||||
(*Empty)(nil), // 6: provisioner.Empty
|
||||
(*ParameterSource)(nil), // 7: provisioner.ParameterSource
|
||||
(*ParameterDestination)(nil), // 8: provisioner.ParameterDestination
|
||||
(*ParameterValue)(nil), // 9: provisioner.ParameterValue
|
||||
(*ParameterSchema)(nil), // 10: provisioner.ParameterSchema
|
||||
(*Log)(nil), // 11: provisioner.Log
|
||||
(*InstanceIdentityAuth)(nil), // 12: provisioner.InstanceIdentityAuth
|
||||
(*Agent)(nil), // 13: provisioner.Agent
|
||||
(*App)(nil), // 14: provisioner.App
|
||||
(*Healthcheck)(nil), // 15: provisioner.Healthcheck
|
||||
(*Resource)(nil), // 16: provisioner.Resource
|
||||
(*Parse)(nil), // 17: provisioner.Parse
|
||||
(*Provision)(nil), // 18: provisioner.Provision
|
||||
nil, // 19: provisioner.Agent.EnvEntry
|
||||
(*Resource_Metadata)(nil), // 20: provisioner.Resource.Metadata
|
||||
(*Parse_Request)(nil), // 21: provisioner.Parse.Request
|
||||
(*Parse_Complete)(nil), // 22: provisioner.Parse.Complete
|
||||
(*Parse_Response)(nil), // 23: provisioner.Parse.Response
|
||||
(*Provision_Metadata)(nil), // 24: provisioner.Provision.Metadata
|
||||
(*Provision_Start)(nil), // 25: provisioner.Provision.Start
|
||||
(*Provision_Cancel)(nil), // 26: provisioner.Provision.Cancel
|
||||
(*Provision_Request)(nil), // 27: provisioner.Provision.Request
|
||||
(*Provision_Complete)(nil), // 28: provisioner.Provision.Complete
|
||||
(*Provision_Response)(nil), // 29: provisioner.Provision.Response
|
||||
}
|
||||
var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{
|
||||
2, // 0: provisioner.ParameterSource.scheme:type_name -> provisioner.ParameterSource.Scheme
|
||||
3, // 1: provisioner.ParameterDestination.scheme:type_name -> provisioner.ParameterDestination.Scheme
|
||||
3, // 2: provisioner.ParameterValue.destination_scheme:type_name -> provisioner.ParameterDestination.Scheme
|
||||
6, // 3: provisioner.ParameterSchema.default_source:type_name -> provisioner.ParameterSource
|
||||
7, // 4: provisioner.ParameterSchema.default_destination:type_name -> provisioner.ParameterDestination
|
||||
4, // 5: provisioner.ParameterSchema.validation_type_system:type_name -> provisioner.ParameterSchema.TypeSystem
|
||||
3, // 0: provisioner.ParameterSource.scheme:type_name -> provisioner.ParameterSource.Scheme
|
||||
4, // 1: provisioner.ParameterDestination.scheme:type_name -> provisioner.ParameterDestination.Scheme
|
||||
4, // 2: provisioner.ParameterValue.destination_scheme:type_name -> provisioner.ParameterDestination.Scheme
|
||||
7, // 3: provisioner.ParameterSchema.default_source:type_name -> provisioner.ParameterSource
|
||||
8, // 4: provisioner.ParameterSchema.default_destination:type_name -> provisioner.ParameterDestination
|
||||
5, // 5: provisioner.ParameterSchema.validation_type_system:type_name -> provisioner.ParameterSchema.TypeSystem
|
||||
0, // 6: provisioner.Log.level:type_name -> provisioner.LogLevel
|
||||
18, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry
|
||||
13, // 8: provisioner.Agent.apps:type_name -> provisioner.App
|
||||
14, // 9: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck
|
||||
12, // 10: provisioner.Resource.agents:type_name -> provisioner.Agent
|
||||
19, // 11: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata
|
||||
9, // 12: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema
|
||||
10, // 13: provisioner.Parse.Response.log:type_name -> provisioner.Log
|
||||
21, // 14: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete
|
||||
1, // 15: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition
|
||||
8, // 16: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue
|
||||
23, // 17: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata
|
||||
24, // 18: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start
|
||||
25, // 19: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel
|
||||
15, // 20: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource
|
||||
10, // 21: provisioner.Provision.Response.log:type_name -> provisioner.Log
|
||||
27, // 22: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete
|
||||
20, // 23: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request
|
||||
26, // 24: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request
|
||||
22, // 25: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response
|
||||
28, // 26: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response
|
||||
25, // [25:27] is the sub-list for method output_type
|
||||
23, // [23:25] is the sub-list for method input_type
|
||||
23, // [23:23] is the sub-list for extension type_name
|
||||
23, // [23:23] is the sub-list for extension extendee
|
||||
0, // [0:23] is the sub-list for field type_name
|
||||
19, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry
|
||||
14, // 8: provisioner.Agent.apps:type_name -> provisioner.App
|
||||
15, // 9: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck
|
||||
1, // 10: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel
|
||||
13, // 11: provisioner.Resource.agents:type_name -> provisioner.Agent
|
||||
20, // 12: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata
|
||||
10, // 13: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema
|
||||
11, // 14: provisioner.Parse.Response.log:type_name -> provisioner.Log
|
||||
22, // 15: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete
|
||||
2, // 16: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition
|
||||
9, // 17: provisioner.Provision.Start.parameter_values:type_name -> provisioner.ParameterValue
|
||||
24, // 18: provisioner.Provision.Start.metadata:type_name -> provisioner.Provision.Metadata
|
||||
25, // 19: provisioner.Provision.Request.start:type_name -> provisioner.Provision.Start
|
||||
26, // 20: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel
|
||||
16, // 21: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource
|
||||
11, // 22: provisioner.Provision.Response.log:type_name -> provisioner.Log
|
||||
28, // 23: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete
|
||||
21, // 24: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request
|
||||
27, // 25: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request
|
||||
23, // 26: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response
|
||||
29, // 27: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response
|
||||
26, // [26:28] is the sub-list for method output_type
|
||||
24, // [24:26] is the sub-list for method input_type
|
||||
24, // [24:24] is the sub-list for extension type_name
|
||||
24, // [24:24] is the sub-list for extension extendee
|
||||
0, // [0:24] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_provisionersdk_proto_provisioner_proto_init() }
|
||||
|
@ -2472,7 +2539,7 @@ func file_provisionersdk_proto_provisioner_proto_init() {
|
|||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc,
|
||||
NumEnums: 5,
|
||||
NumEnums: 6,
|
||||
NumMessages: 24,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
|
|
|
@ -87,6 +87,12 @@ message Agent {
|
|||
}
|
||||
}
|
||||
|
||||
enum AppSharingLevel {
|
||||
OWNER = 0;
|
||||
AUTHENTICATED = 1;
|
||||
PUBLIC = 2;
|
||||
}
|
||||
|
||||
// App represents a dev-accessible application on the workspace.
|
||||
message App {
|
||||
string name = 1;
|
||||
|
@ -95,6 +101,7 @@ message App {
|
|||
string icon = 4;
|
||||
bool subdomain = 5;
|
||||
Healthcheck healthcheck = 6;
|
||||
AppSharingLevel sharing_level = 7;
|
||||
}
|
||||
|
||||
// Healthcheck represents configuration for checking for app readiness.
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface APIKey {
|
|||
readonly created_at: string
|
||||
readonly updated_at: string
|
||||
readonly login_type: LoginType
|
||||
readonly scope: APIKeyScope
|
||||
readonly lifetime_seconds: number
|
||||
}
|
||||
|
||||
|
@ -218,6 +219,11 @@ export interface CreateTestAuditLogRequest {
|
|||
readonly resource_id?: string
|
||||
}
|
||||
|
||||
// From codersdk/apikey.go
|
||||
export interface CreateTokenRequest {
|
||||
readonly scope: APIKeyScope
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface CreateUserRequest {
|
||||
readonly email: string
|
||||
|
@ -344,7 +350,7 @@ export interface Feature {
|
|||
readonly actual?: number
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
// From codersdk/apikey.go
|
||||
export interface GenerateAPIKeyResponse {
|
||||
readonly key: string
|
||||
}
|
||||
|
@ -783,6 +789,7 @@ export interface WorkspaceApp {
|
|||
readonly command?: string
|
||||
readonly icon?: string
|
||||
readonly subdomain: boolean
|
||||
readonly sharing_level: WorkspaceAppSharingLevel
|
||||
readonly healthcheck: Healthcheck
|
||||
readonly health: WorkspaceAppHealth
|
||||
}
|
||||
|
@ -851,6 +858,9 @@ export interface WorkspaceResourceMetadata {
|
|||
readonly sensitive: boolean
|
||||
}
|
||||
|
||||
// From codersdk/apikey.go
|
||||
export type APIKeyScope = "all" | "application_connect"
|
||||
|
||||
// From codersdk/audit.go
|
||||
export type AuditAction = "create" | "delete" | "write"
|
||||
|
||||
|
@ -931,6 +941,9 @@ export type WorkspaceAppHealth =
|
|||
| "initializing"
|
||||
| "unhealthy"
|
||||
|
||||
// From codersdk/workspaceapps.go
|
||||
export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public"
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export type WorkspaceStatus =
|
||||
| "canceled"
|
||||
|
|
|
@ -199,6 +199,7 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = {
|
|||
icon: "",
|
||||
subdomain: false,
|
||||
health: "disabled",
|
||||
sharing_level: "owner",
|
||||
healthcheck: {
|
||||
url: "",
|
||||
interval: 0,
|
||||
|
|
Loading…
Reference in New Issue