Compare commits

...

6 Commits

Author SHA1 Message Date
Cian Johnston 43e4897d23 chore(go.mod): update x/crypto to 0.31.0 (#15869) (#15872)
(cherry picked from commit 14ce3aa018)
2024-12-16 13:42:20 +00:00
Cian Johnston fa0ce565b6 fix(site/static/icon): add filebrowser icon (#15367) (#15641)
Fixes https://github.com/coder/coder/issues/15365

We used to hit
https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg
for the filebrowser icon but coder/modules#334 modified the icon URL to
point to a self-hosted icon.

I simply copied the icon from the `coder/modules` repo.

(cherry picked from commit dc29b81286)
2024-11-25 10:56:22 +00:00
Jon Ayers 379ced672e fix(site): sanitize login redirect (#15208) (#15219)
Co-authored-by: Colin Adler <colin1adler@gmail.com>
2024-10-24 20:48:48 +01:00
Jon Ayers 971b1a87bd fix: fix bug with trailing version info not being properly stripped (… (#15223)
…#14963)

Fixes a bug where excess version info was not being stripped properly
from documentation links.

Co-authored-by: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com>
2024-10-24 20:47:12 +01:00
Jon Ayers 5133315792 fix: fix error handling to prevent spam in proc prio management (#15071) (#15098) 2024-10-15 18:41:13 -05:00
Stephen Kirby 683a7209b5 chore: cherry pick updates for v2.16.0 (#14919)
- [x] https://github.com/coder/coder/pull/14869
- [x] https://github.com/coder/coder/pull/14778
- [x] https://github.com/coder/coder/pull/14883

Addition to resolve flake: 
- [x] https://github.com/coder/coder/pull/14875

---------

Co-authored-by: Danny Kopping <dannykopping@gmail.com>
Co-authored-by: Garrett Delfosse <garrett@coder.com>
Co-authored-by: Ben Potter <ben@coder.com>
Co-authored-by: Spike Curtis <spike@coder.com>
2024-10-01 13:22:34 -05:00
78 changed files with 1124 additions and 569 deletions
+3 -3
View File
@@ -1674,7 +1674,7 @@ func (a *agent) manageProcessPriority(ctx context.Context, debouncer *logDebounc
}
score, niceErr := proc.Niceness(a.syscaller)
if !isBenignProcessErr(niceErr) {
if niceErr != nil && !isBenignProcessErr(niceErr) {
debouncer.Warn(ctx, "unable to get proc niceness",
slog.F("cmd", proc.Cmd()),
slog.F("pid", proc.PID),
@@ -1693,7 +1693,7 @@ func (a *agent) manageProcessPriority(ctx context.Context, debouncer *logDebounc
if niceErr == nil {
err := proc.SetNiceness(a.syscaller, niceness)
if !isBenignProcessErr(err) {
if err != nil && !isBenignProcessErr(err) {
debouncer.Warn(ctx, "unable to set proc niceness",
slog.F("cmd", proc.Cmd()),
slog.F("pid", proc.PID),
@@ -1707,7 +1707,7 @@ func (a *agent) manageProcessPriority(ctx context.Context, debouncer *logDebounc
if oomScore != unsetOOMScore && oomScore != proc.OOMScoreAdj && !isCustomOOMScore(agentScore, proc) {
oomScoreStr := strconv.Itoa(oomScore)
err := afero.WriteFile(a.filesystem, fmt.Sprintf("/proc/%d/oom_score_adj", proc.PID), []byte(oomScoreStr), 0o644)
if !isBenignProcessErr(err) {
if err != nil && !isBenignProcessErr(err) {
debouncer.Warn(ctx, "unable to set oom_score_adj",
slog.F("cmd", proc.Cmd()),
slog.F("pid", proc.PID),
-1
View File
@@ -20,7 +20,6 @@ func createOpts(t *testing.T) *coderdtest.Options {
t.Helper()
dt := coderdtest.DeploymentValues(t)
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
return &coderdtest.Options{
DeploymentValues: dt,
}
+42 -52
View File
@@ -56,15 +56,16 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/v2/coderd/entitlements"
"github.com/coder/coder/v2/coderd/notifications/reports"
"github.com/coder/coder/v2/coderd/runtimeconfig"
"github.com/coder/pretty"
"github.com/coder/quartz"
"github.com/coder/retry"
"github.com/coder/serpent"
"github.com/coder/wgtunnel/tunnelsdk"
"github.com/coder/coder/v2/coderd/entitlements"
"github.com/coder/coder/v2/coderd/notifications/reports"
"github.com/coder/coder/v2/coderd/runtimeconfig"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clilog"
"github.com/coder/coder/v2/cli/cliui"
@@ -679,10 +680,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
options.OIDCConfig = oc
}
experiments := coderd.ReadExperiments(
options.Logger, options.DeploymentValues.Experiments.Value(),
)
// We'll read from this channel in the select below that tracks shutdown. If it remains
// nil, that case of the select will just never fire, but it's important not to have a
// "bare" read on this channel.
@@ -946,6 +943,33 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return xerrors.Errorf("write config url: %w", err)
}
// Manage notifications.
cfg := options.DeploymentValues.Notifications
metrics := notifications.NewMetrics(options.PrometheusRegistry)
helpers := templateHelpers(options)
// The enqueuer is responsible for enqueueing notifications to the given store.
enqueuer, err := notifications.NewStoreEnqueuer(cfg, options.Database, helpers, logger.Named("notifications.enqueuer"), quartz.NewReal())
if err != nil {
return xerrors.Errorf("failed to instantiate notification store enqueuer: %w", err)
}
options.NotificationsEnqueuer = enqueuer
// The notification manager is responsible for:
// - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications)
// - keeping the store updated with status updates
notificationsManager, err := notifications.NewManager(cfg, options.Database, helpers, metrics, logger.Named("notifications.manager"))
if err != nil {
return xerrors.Errorf("failed to instantiate notification manager: %w", err)
}
// nolint:gocritic // TODO: create own role.
notificationsManager.Run(dbauthz.AsSystemRestricted(ctx))
// Run report generator to distribute periodic reports.
notificationReportGenerator := reports.NewReportGenerator(ctx, logger.Named("notifications.report_generator"), options.Database, options.NotificationsEnqueuer, quartz.NewReal())
defer notificationReportGenerator.Close()
// Since errCh only has one buffered slot, all routines
// sending on it must be wrapped in a select/default to
// avoid leaving dangling goroutines waiting for the
@@ -1002,38 +1026,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
options.WorkspaceUsageTracker = tracker
defer tracker.Close()
// Manage notifications.
var (
notificationsManager *notifications.Manager
)
if experiments.Enabled(codersdk.ExperimentNotifications) {
cfg := options.DeploymentValues.Notifications
metrics := notifications.NewMetrics(options.PrometheusRegistry)
helpers := templateHelpers(options)
// The enqueuer is responsible for enqueueing notifications to the given store.
enqueuer, err := notifications.NewStoreEnqueuer(cfg, options.Database, helpers, logger.Named("notifications.enqueuer"), quartz.NewReal())
if err != nil {
return xerrors.Errorf("failed to instantiate notification store enqueuer: %w", err)
}
options.NotificationsEnqueuer = enqueuer
// The notification manager is responsible for:
// - creating notifiers and managing their lifecycles (notifiers are responsible for dequeueing/sending notifications)
// - keeping the store updated with status updates
notificationsManager, err = notifications.NewManager(cfg, options.Database, helpers, metrics, logger.Named("notifications.manager"))
if err != nil {
return xerrors.Errorf("failed to instantiate notification manager: %w", err)
}
// nolint:gocritic // TODO: create own role.
notificationsManager.Run(dbauthz.AsSystemRestricted(ctx))
// Run report generator to distribute periodic reports.
notificationReportGenerator := reports.NewReportGenerator(ctx, logger.Named("notifications.report_generator"), options.Database, options.NotificationsEnqueuer, quartz.NewReal())
defer notificationReportGenerator.Close()
}
// Wrap the server in middleware that redirects to the access URL if
// the request is not to a local IP.
var handler http.Handler = coderAPI.RootHandler
@@ -1153,19 +1145,17 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// Cancel any remaining in-flight requests.
shutdownConns()
if notificationsManager != nil {
// Stop the notification manager, which will cause any buffered updates to the store to be flushed.
// If the Stop() call times out, messages that were sent but not reflected as such in the store will have
// their leases expire after a period of time and will be re-queued for sending.
// See CODER_NOTIFICATIONS_LEASE_PERIOD.
cliui.Info(inv.Stdout, "Shutting down notifications manager..."+"\n")
err = shutdownWithTimeout(notificationsManager.Stop, 5*time.Second)
if err != nil {
cliui.Warnf(inv.Stderr, "Notifications manager shutdown took longer than 5s, "+
"this may result in duplicate notifications being sent: %s\n", err)
} else {
cliui.Info(inv.Stdout, "Gracefully shut down notifications manager\n")
}
// Stop the notification manager, which will cause any buffered updates to the store to be flushed.
// If the Stop() call times out, messages that were sent but not reflected as such in the store will have
// their leases expire after a period of time and will be re-queued for sending.
// See CODER_NOTIFICATIONS_LEASE_PERIOD.
cliui.Info(inv.Stdout, "Shutting down notifications manager..."+"\n")
err = shutdownWithTimeout(notificationsManager.Stop, 5*time.Second)
if err != nil {
cliui.Warnf(inv.Stderr, "Notifications manager shutdown took longer than 5s, "+
"this may result in duplicate notifications being sent: %s\n", err)
} else {
cliui.Info(inv.Stdout, "Gracefully shut down notifications manager\n")
}
// Shut down provisioners before waiting for WebSockets
+4 -6
View File
@@ -37,11 +37,12 @@ import (
"tailscale.com/util/singleflight"
"cdr.dev/slog"
"github.com/coder/quartz"
"github.com/coder/serpent"
"github.com/coder/coder/v2/coderd/entitlements"
"github.com/coder/coder/v2/coderd/idpsync"
"github.com/coder/coder/v2/coderd/runtimeconfig"
"github.com/coder/quartz"
"github.com/coder/serpent"
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/buildinfo"
@@ -1257,10 +1258,7 @@ func New(options *Options) *API {
})
})
r.Route("/notifications", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentNotifications),
)
r.Use(apiKeyMiddleware)
r.Get("/settings", api.notificationsSettings)
r.Put("/settings", api.putNotificationsSettings)
r.Route("/templates", func(r chi.Router) {
+7 -2
View File
@@ -54,6 +54,7 @@ type Manager struct {
runOnce sync.Once
stopOnce sync.Once
doneOnce sync.Once
stop chan any
done chan any
@@ -153,7 +154,9 @@ func (m *Manager) Run(ctx context.Context) {
// events, creating a notifier, and publishing bulk dispatch result updates to the store.
func (m *Manager) loop(ctx context.Context) error {
defer func() {
close(m.done)
m.doneOnce.Do(func() {
close(m.done)
})
m.log.Info(context.Background(), "notification manager stopped")
}()
@@ -364,7 +367,9 @@ func (m *Manager) Stop(ctx context.Context) error {
// If the notifier hasn't been started, we don't need to wait for anything.
// This is only really during testing when we want to enqueue messages only but not deliver them.
if m.notifier == nil {
close(m.done)
m.doneOnce.Do(func() {
close(m.done)
})
} else {
m.notifier.stop()
}
@@ -1187,7 +1187,6 @@ func createOpts(t *testing.T) *coderdtest.Options {
t.Helper()
dt := coderdtest.DeploymentValues(t)
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
return &coderdtest.Options{
DeploymentValues: dt,
}
+1 -1
View File
@@ -49,7 +49,7 @@ func NewReportGenerator(ctx context.Context, logger slog.Logger, db database.Sto
return nil
}
err = reportFailedWorkspaceBuilds(ctx, logger, db, enqueuer, clk)
err = reportFailedWorkspaceBuilds(ctx, logger, tx, enqueuer, clk)
if err != nil {
return xerrors.Errorf("unable to generate reports with failed workspace builds: %w", err)
}
-1
View File
@@ -20,7 +20,6 @@ func createOpts(t *testing.T) *coderdtest.Options {
t.Helper()
dt := coderdtest.DeploymentValues(t)
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
return &coderdtest.Options{
DeploymentValues: dt,
}
+6 -2
View File
@@ -804,8 +804,12 @@ func DefaultSupportLinks(docsURL string) []LinkConfig {
}
}
func removeTrailingVersionInfo(v string) string {
return strings.Split(strings.Split(v, "-")[0], "+")[0]
}
func DefaultDocsURL() string {
version := strings.Split(buildinfo.Version(), "-")[0]
version := removeTrailingVersionInfo(buildinfo.Version())
if version == "v0.0.0" {
return "https://coder.com/docs"
}
@@ -2901,7 +2905,7 @@ const (
// users to opt-in to via --experimental='*'.
// Experiments that are not ready for consumption by all users should
// not be included here and will be essentially hidden.
var ExperimentsAll = Experiments{ExperimentNotifications}
var ExperimentsAll = Experiments{}
// Experiments is a list of experiments.
// Multiple experiments may be enabled at the same time.
+36
View File
@@ -0,0 +1,36 @@
package codersdk
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestRemoveTrailingVersionInfo(t *testing.T) {
t.Parallel()
testCases := []struct {
Version string
ExpectedAfterStrippingInfo string
}{
{
Version: "v2.16.0+683a720",
ExpectedAfterStrippingInfo: "v2.16.0",
},
{
Version: "v2.16.0-devel+683a720",
ExpectedAfterStrippingInfo: "v2.16.0",
},
{
Version: "v2.16.0+683a720-devel",
ExpectedAfterStrippingInfo: "v2.16.0",
},
}
for _, tc := range testCases {
tc := tc
stripped := removeTrailingVersionInfo(tc.Version)
require.Equal(t, tc.ExpectedAfterStrippingInfo, stripped)
}
}
+6 -2
View File
@@ -12,6 +12,8 @@ import (
"github.com/google/uuid"
"github.com/hashicorp/yamux"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
@@ -278,9 +280,11 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
type ProvisionerKeyTags map[string]string
func (p ProvisionerKeyTags) String() string {
keys := maps.Keys(p)
slices.Sort(keys)
tags := []string{}
for key, value := range p {
tags = append(tags, fmt.Sprintf("%s=%s", key, value))
for _, key := range keys {
tags = append(tags, fmt.Sprintf("%s=%s", key, p[key]))
}
return strings.Join(tags, " ")
}
+1 -5
View File
@@ -1,4 +1,4 @@
# Appearance (enterprise)
# Appearance (enterprise) (premium)
Customize the look of your Coder deployment to meet your enterprise
requirements.
@@ -93,7 +93,3 @@ For CLI, use,
export CODER_SUPPORT_LINKS='[{"name": "Hello GitHub", "target": "https://github.com/coder/coder", "icon": "bug"}, {"name": "Hello Slack", "target": "https://codercom.slack.com/archives/C014JH42DBJ", "icon": "https://raw.githubusercontent.com/coder/coder/main/site/static/icon/slack.svg"}, {"name": "Hello Discord", "target": "https://discord.gg/coder", "icon": "https://raw.githubusercontent.com/coder/coder/main/site/static/icon/discord.svg"}, {"name": "Hello Foobar", "target": "https://discord.gg/coder", "icon": "/emojis/1f3e1.png"}]'
coder-server
```
## Up next
- [Enterprise](../enterprise.md)
+2 -2
View File
@@ -122,5 +122,5 @@ entry:
## Enabling this feature
This feature is only available with an enterprise license.
[Learn more](../enterprise.md)
This feature is only available with a
[Premium or Enterprise license](https://coder.com/pricing).
+290 -30
View File
@@ -1,7 +1,5 @@
# Authentication
![OIDC with Coder Sequence Diagram](../images/oidc-sequence-diagram.svg).
By default, Coder is accessible via password authentication. Coder does not
recommend using password authentication in production, and recommends using an
authentication provider with properly configured multi-factor authentication
@@ -227,7 +225,7 @@ your Coder deployment:
CODER_DISABLE_PASSWORD_AUTH=true
```
## SCIM (enterprise)
## SCIM (enterprise) (premium)
Coder supports user provisioning and deprovisioning via SCIM 2.0 with header
authentication. Upon deactivation, users are
@@ -249,36 +247,50 @@ CODER_TLS_CLIENT_CERT_FILE=/path/to/cert.pem
CODER_TLS_CLIENT_KEY_FILE=/path/to/key.pem
```
## Group Sync (enterprise)
## Group Sync (enterprise) (premium)
If your OpenID Connect provider supports group claims, you can configure Coder
to synchronize groups in your auth provider to groups within Coder.
to synchronize groups in your auth provider to groups within Coder. To enable
group sync, ensure that the `groups` claim is being sent by your OpenID
provider. You might need to request an additional
[scope](../reference/cli/server.md#--oidc-scopes) or additional configuration on
the OpenID provider side.
To enable group sync, ensure that the `groups` claim is set by adding the
correct scope to request. If group sync is enabled, the user's groups will be
controlled by the OIDC provider. This means manual group additions/removals will
be overwritten on the next login.
If group sync is enabled, the user's groups will be controlled by the OIDC
provider. This means manual group additions/removals will be overwritten on the
next user login.
```env
# as an environment variable
CODER_OIDC_SCOPES=openid,profile,email,groups
There are two ways you can configure group sync:
<div class="tabs">
## Server Flags
First, confirm that your OIDC provider is sending claims by logging in with OIDC
and visiting the following URL with an `Owner` account:
```text
https://[coder.example.com]/api/v2/debug/[your-username]/debug-link
```
```shell
# as a flag
--oidc-scopes openid,profile,email,groups
```
You should see a field in either `id_token_claims`, `user_info_claims` or both
followed by a list of the user's OIDC groups in the response. This is the
[claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) sent by
the OIDC provider. See
[Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug this.
With the `groups` scope requested, we also need to map the `groups` claim name.
Coder recommends using `groups` for the claim name. This step is necessary if
your **scope's name** is something other than `groups`.
> Depending on the OIDC provider, this claim may be named differently. Common
> ones include `groups`, `memberOf`, and `roles`.
```env
Next configure the Coder server to read groups from the claim name with the
[OIDC group field](../reference/cli/server.md#--oidc-group-field) server flag:
```sh
# as an environment variable
CODER_OIDC_GROUP_FIELD=groups
```
```shell
```sh
# as a flag
--oidc-group-field groups
```
@@ -288,14 +300,16 @@ names in Coder and removed from groups that the user no longer belongs to.
For cases when an OIDC provider only returns group IDs ([Azure AD][azure-gids])
or you want to have different group names in Coder than in your OIDC provider,
you can configure mapping between the two.
you can configure mapping between the two with the
[OIDC group mapping](../reference/cli/server.md#--oidc-group-mapping) server
flag.
```env
```sh
# as an environment variable
CODER_OIDC_GROUP_MAPPING='{"myOIDCGroupID": "myCoderGroupName"}'
```
```shell
```sh
# as a flag
--oidc-group-mapping '{"myOIDCGroupID": "myCoderGroupName"}'
```
@@ -313,11 +327,103 @@ coder:
From the example above, users that belong to the `myOIDCGroupID` group in your
OIDC provider will be added to the `myCoderGroupName` group in Coder.
> **Note:** Groups are only updated on login.
[azure-gids]:
https://github.com/MicrosoftDocs/azure-docs/issues/59766#issuecomment-664387195
## Runtime (Organizations)
> Note: You must have a Premium license with Organizations enabled to use this.
> [Contact your account team](https://coder.com/contact) for more details
For deployments with multiple [organizations](./organizations.md), you must
configure group sync at the organization level. In future Coder versions, you
will be able to configure this in the UI. For now, you must use CLI commands.
First confirm you have the [Coder CLI](../install/index.md) installed and are
logged in with a user who is an Owner or Organization Admin role. Next, confirm
that your OIDC provider is sending a groups claim by logging in with OIDC and
visiting the following URL:
```text
https://[coder.example.com]/api/v2/debug/[your-username]/debug-link
```
You should see a field in either `id_token_claims`, `user_info_claims` or both
followed by a list of the user's OIDC groups in the response. This is the
[claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) sent by
the OIDC provider. See
[Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug this.
> Depending on the OIDC provider, this claim may be named differently. Common
> ones include `groups`, `memberOf`, and `roles`.
To fetch the current group sync settings for an organization, run the following:
```sh
coder organizations settings show group-sync \
--org <org-name> \
> group-sync.json
```
The default for an organization looks like this:
```json
{
"field": "",
"mapping": null,
"regex_filter": null,
"auto_create_missing_groups": false
}
```
Below is an example that uses the `groups` claim and maps all groups prefixed by
`coder-` into Coder:
```json
{
"field": "groups",
"mapping": null,
"regex_filter": "^coder-.*$",
"auto_create_missing_groups": true
}
```
> Note: You much specify Coder group IDs instead of group names. The fastest way
> to find the ID for a corresponding group is by visiting
> `https://coder.example.com/api/v2/groups`.
Here is another example which maps `coder-admins` from the identity provider to
2 groups in Coder and `coder-users` from the identity provider to another group:
```json
{
"field": "groups",
"mapping": {
"coder-admins": [
"2ba2a4ff-ddfb-4493-b7cd-1aec2fa4c830",
"93371154-150f-4b12-b5f0-261bb1326bb4"
],
"coder-users": ["2f4bde93-0179-4815-ba50-b757fb3d43dd"]
},
"regex_filter": null,
"auto_create_missing_groups": false
}
```
To set these group sync settings, use the following command:
```sh
coder organizations settings set group-sync \
--org <org-name> \
< group-sync.json
```
Visit the Coder UI to confirm these changes:
![IDP Sync](../images/admin/organizations/group-sync.png)
</div>
### Group allowlist
You can limit which groups from your identity provider can log in to Coder with
@@ -326,11 +432,36 @@ Users who are not in a matching group will see the following error:
![Unauthorized group error](../images/admin/group-allowlist.png)
## Role sync (enterprise)
## Role sync (enterprise) (premium)
If your OpenID Connect provider supports roles claims, you can configure Coder
to synchronize roles in your auth provider to deployment-wide roles within
Coder.
to synchronize roles in your auth provider to roles within Coder.
There are 2 ways to do role sync. Server Flags assign site wide roles, and
runtime org role sync assigns organization roles
<div class="tabs">
## Server Flags
First, confirm that your OIDC provider is sending a roles claim by logging in
with OIDC and visiting the following URL with an `Owner` account:
```text
https://[coder.example.com]/api/v2/debug/[your-username]/debug-link
```
You should see a field in either `id_token_claims`, `user_info_claims` or both
followed by a list of the user's OIDC roles in the response. This is the
[claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) sent by
the OIDC provider. See
[Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug this.
> Depending on the OIDC provider, this claim may be named differently.
Next configure the Coder server to read groups from the claim name with the
[OIDC role field](../reference/cli/server.md#--oidc-user-role-field) server
flag:
Set the following in your Coder server [configuration](./configure.md).
@@ -346,7 +477,136 @@ CODER_OIDC_USER_ROLE_MAPPING='{"TemplateAuthor":["template-admin","user-admin"]}
> One role from your identity provider can be mapped to many roles in Coder
> (e.g. the example above maps to 2 roles in Coder.)
## Troubleshooting group/role sync
## Runtime (Organizations)
> Note: You must have a Premium license with Organizations enabled to use this.
> [Contact your account team](https://coder.com/contact) for more details
For deployments with multiple [organizations](./organizations.md), you can
configure role sync at the organization level. In future Coder versions, you
will be able to configure this in the UI. For now, you must use CLI commands.
First, confirm that your OIDC provider is sending a roles claim by logging in
with OIDC and visiting the following URL with an `Owner` account:
```text
https://[coder.example.com]/api/v2/debug/[your-username]/debug-link
```
You should see a field in either `id_token_claims`, `user_info_claims` or both
followed by a list of the user's OIDC roles in the response. This is the
[claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) sent by
the OIDC provider. See
[Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug this.
> Depending on the OIDC provider, this claim may be named differently.
To fetch the current group sync settings for an organization, run the following:
```sh
coder organizations settings show role-sync \
--org <org-name> \
> role-sync.json
```
The default for an organization looks like this:
```json
{
"field": "",
"mapping": null
}
```
Below is an example that uses the `roles` claim and maps `coder-admins` from the
IDP as an `Organization Admin` and also maps to a custom `provisioner-admin`
role.
```json
{
"field": "roles",
"mapping": {
"coder-admins": ["organization-admin"],
"infra-admins": ["provisioner-admin"]
}
}
```
> Note: Be sure to use the `name` field for each role, not the display name. Use
> `coder organization roles show --org=<your-org>` to see roles for your
> organization.
To set these role sync settings, use the following command:
```sh
coder organizations settings set role-sync \
--org <org-name> \
< role-sync.json
```
Visit the Coder UI to confirm these changes:
![IDP Sync](../images/admin/organizations/role-sync.png)
</div>
## Organization Sync (Premium)
> Note: In a future Coder release, this can be managed via the Coder UI instead
> of server flags.
If your OpenID Connect provider supports groups/role claims, you can configure
Coder to synchronize claims in your auth provider to organizations within Coder.
First, confirm that your OIDC provider is sending clainms by logging in with
OIDC and visiting the following URL with an `Owner` account:
```text
https://[coder.example.com]/api/v2/debug/[your-username]/debug-link
```
You should see a field in either `id_token_claims`, `user_info_claims` or both
followed by a list of the user's OIDC groups in the response. This is the
[claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) sent by
the OIDC provider. See
[Troubleshooting](#troubleshooting-grouproleorganization-sync) to debug this.
> Depending on the OIDC provider, this claim may be named differently. Common
> ones include `groups`, `memberOf`, and `roles`.
Next configure the Coder server to read groups from the claim name with the
[OIDC organization field](../reference/cli/server.md#--oidc-organization-field)
server flag:
```sh
# as an environment variable
CODER_OIDC_ORGANIZATION_FIELD=groups
```
Next, fetch the corresponding organization IDs using the following endpoint:
```text
https://[coder.example.com]/api/v2/organizations
```
Set the following in your Coder server [configuration](./configure.md).
```env
CODER_OIDC_ORGANIZATION_MAPPING='{"data-scientists":["d8d9daef-e273-49ff-a832-11fe2b2d4ab1", "70be0908-61b5-4fb5-aba4-4dfb3a6c5787"]}'
```
> One claim value from your identity provider can be mapped to many
> organizations in Coder (e.g. the example above maps to 2 organizations in
> Coder.)
By default, all users are assigned to the default (first) organization. You can
disable that with:
```env
CODER_OIDC_ORGANIZATION_ASSIGN_DEFAULT=false
```
## Troubleshooting group/role/organization sync
Some common issues when enabling group/role sync.
+3 -3
View File
@@ -195,10 +195,10 @@ Optionally, you can request custom scopes:
CODER_EXTERNAL_AUTH_0_SCOPES="repo:read repo:write write:gpg_key"
```
### Multiple External Providers (enterprise)
### Multiple External Providers (enterprise) (premium)
Multiple providers are an Enterprise feature. [Learn more](../enterprise.md).
Below is an example configuration with multiple providers.
Multiple providers are an [Enterprise feature](https://coder.com/pricing). Below
is an example configuration with multiple providers.
```env
# Provider 1) github.com
+2 -2
View File
@@ -9,5 +9,5 @@ access to specific templates. They can be defined via the Coder web UI,
## Enabling this feature
This feature is only available with an enterprise license.
[Learn more](../enterprise.md)
This feature is only available with a
[Premium or Enterprise license](https://coder.com/pricing).
-1
View File
@@ -73,4 +73,3 @@ Then, increase the number of pods.
- [Networking](../networking/index.md)
- [Kubernetes](../install/kubernetes.md)
- [Enterprise](../enterprise.md)
+1 -1
View File
@@ -231,7 +231,7 @@ notification is indicated on the right hand side of this table.
![User Notification Preferences](../images/user-notification-preferences.png)
## Delivery Preferences (enterprise)
## Delivery Preferences (enterprise) (premium)
Administrators can configure which delivery methods are used for each different
[event type](#event-types).
+110
View File
@@ -0,0 +1,110 @@
# Organizations (Premium)
> Note: Organizations requires a [Premium license](../licensing.md). For more
> details, [contact your account team](https://coder.com/contact).
Organizations can be used to segment and isolate resources inside a Coder
deployment for different user groups or projects.
## Example
Here is an example of how one could use organizations to run a Coder deployment
with multiple platform teams, all with unique resources:
![Organizations Example](../images/admin/organizations/diagram.png)
## The default organization
All Coder deployments start with one organization called `Coder`.
To edit the organization details, navigate to `Deployment -> Organizations` in
the top bar:
![Organizations Menu](../images/admin/organizations/deployment-organizations.png)
From there, you can manage the name, icon, description, users, and groups:
![Organization Settings](../images/admin/organizations/default-organization.png)
## Additional organizations
Any additional organizations have unique admins, users, templates, provisioners,
groups, and workspaces. Each organization must have at least one
[provisioner](./provisioners.md) as the built-in provisioner only applies to the
default organization.
You can configure [organization/role/group sync](./auth.md) from your identity
provider to avoid manually assigning users to organizations.
## Creating an organization
### Prerequisites
- Coder v2.16+ deployment with Premium license with Organizations enabled
([contact your account team](https://coder.com/contact)) for more details.
- User with `Owner` role
### 1. Create the organization
Within the sidebar, click `New organization` to create an organization. In this
example, we'll create the `data-platform` org.
![New Organization](../images/admin/organizations/new-organization.png)
From there, let's deploy a provisioner and template for this organization.
### 2. Deploy a provisioner
[Provisioners](../admin/provisioners.md) are organization-scoped and are
responsible for executing Terraform/OpenTofu to provision the infrastructure for
workspaces and testing templates. Before creating templates, we must deploy at
least one provisioner as the built-in provisioners are scoped to the default
organization.
Using Coder CLI, run the following command to create a key that will be used to
authenticate the provisioner:
```sh
coder provisioner keys create data-cluster-key --org data-platform
Successfully created provisioner key data-cluster! Save this authentication token, it will not be shown again.
< key omitted >
```
Next, start the provisioner with the key on your desired platform. In this
example, we'll start it using the Coder CLI on a host with Docker. For
instructions on using other platforms like Kubernetes, see our
[provisioner documentation](../admin/provisioners.md).
```sh
export CODER_URL=https://<your-coder-url>
export CODER_PROVISIONER_DAEMON_KEY=<key>
coder provisionerd start --org <org-name>
```
### 3. Create a template
Once you've started a provisioner, you can create a template. You'll notice the
"Create Template" screen now has an organization dropdown:
![Template Org Picker](../images/admin/organizations/template-org-picker.png)
### 5. Add members
Navigate to `Deployment->Organizations` to add members to your organization.
Once added, they will be able to see the organization-specific templates.
![Add members](../images/admin/organizations/organization-members.png)
### 6. Create a workspace
Now, users in the data platform organization will see the templates related to
their organization. Users can be in multiple organizations.
![Workspace List](../images/admin/organizations/workspace-list.png)
## Beta
Organizations is in beta. If you encounter any issues, please
[file an issue](https://github.com/coder/coder/issues/new) or contact your
account team.
+151 -78
View File
@@ -3,10 +3,10 @@
By default, the Coder server runs
[built-in provisioner daemons](../reference/cli/server.md#provisioner-daemons),
which execute `terraform` during workspace and template builds. However, there
are sometimes benefits to running external provisioner daemons:
are often benefits to running external provisioner daemons:
- **Secure build environments:** Run build jobs in isolated containers,
preventing malicious templates from gaining shell access to the Coder host.
preventing malicious templates from gaining sh access to the Coder host.
- **Isolate APIs:** Deploy provisioners in isolated environments (on-prem, AWS,
Azure) instead of exposing APIs (Docker, Kubernetes, VMware) to the Coder
@@ -20,82 +20,101 @@ are sometimes benefits to running external provisioner daemons:
times from the Coder server. See
[Scaling Coder](scaling/scale-utility.md#recent-scale-tests) for more details.
Each provisioner can run a single
[concurrent workspace build](scaling/scale-testing.md#control-plane-provisionerd).
Each provisioner runs a single
[concurrent workspace build](scaling/scale-testing.md#control-plane-provisioner).
For example, running 30 provisioner containers will allow 30 users to start
workspaces at the same time.
Provisioners are started with the
[coder provisionerd start](../reference/cli/provisioner_start.md) command.
[`coder provisioner start`](../reference/cli/provisioner_start.md) command in
the [full Coder binary](https://github.com/coder/coder/releases). Keep reading
to learn how to start provisioners via Docker, Kubernetes, Systemd, etc.
## Authentication
The provisioner daemon must authenticate with your Coder deployment.
The provisioner daemon must authenticate with your Coder deployment. If you have
multiple [organizations](./organizations.md), you'll need at least 1 provisioner
running for each organization.
Set a
<div class="tabs">
## Scoped Key (Recommended)
We recommend creating finely-scoped keys for provisioners. Keys are scoped to an
organization.
```sh
coder provisioner keys create my-key \
--org default
Successfully created provisioner key my-key! Save this authentication token, it will not be shown again.
<key omitted>
```
Or, restrict the provisioner to jobs with specific tags
```sh
coder provisioner keys create kubernetes-key \
--org default \
--tag environment=kubernetes
Successfully created provisioner key kubernetes-key! Save this authentication token, it will not be shown again.
<key omitted>
```
To start the provisioner:
```sh
export CODER_URL=https://<your-coder-url>
export CODER_PROVISIONER_DAEMON_KEY=<key>
coder provisioner start
```
Keep reading to see instructions for running provisioners on
Kubernetes/Docker/etc.
## User Tokens
A user account with the role `Template Admin` or `Owner` can start provisioners
using their user account. This may be beneficial if you are running provisioners
via [automation](./automation.md).
```sh
coder login https://<your-coder-url>
coder provisioner start
```
To start a provisioner with specific tags:
```sh
coder login https://<your-coder-url>
coder provisioner start \
--tag environment=kubernetes
```
Note: Any user can start [user-scoped provisioners](#User-scoped-Provisioners),
but this will also require a template on your deployment with the corresponding
tags.
## Global PSK
A deployment-wide PSK can be used to authenticate any provisioner. We do not
recommend this approach anymore, as it makes key rotation or isolating
provisioners far more difficult. To use a global PSK, set a
[provisioner daemon pre-shared key (PSK)](../reference/cli/server.md#--provisioner-daemon-psk)
on the Coder server and start the provisioner with
`coder provisionerd start --psk <your-psk>`. If you are
[installing with Helm](../install/kubernetes.md#install-coder-with-helm), see
the [Helm example](#example-running-an-external-provisioner-with-helm) below.
on the Coder server.
> Coder still supports authenticating the provisioner daemon with a
> [token](../reference/cli/README.md#--token) from a user with the Template
> Admin or Owner role. This method is deprecated in favor of the PSK, which only
> has permission to access provisioner daemon APIs. We recommend migrating to
> the PSK as soon as practical.
Next, start the provisioner:
## Types of provisioners
Provisioners can broadly be categorized by scope: `organization` or `user`. The
scope of a provisioner can be specified with
[`-tag=scope=<scope>`](../reference/cli/provisioner_start.md#t---tag) when
starting the provisioner daemon. Only users with at least the
[Template Admin](../admin/users.md#roles) role or higher may create
organization-scoped provisioner daemons.
There are two exceptions:
- [Built-in provisioners](../reference/cli/server.md#provisioner-daemons) are
always organization-scoped.
- External provisioners started using a
[pre-shared key (PSK)](../reference/cli/provisioner_start.md#psk) are always
organization-scoped.
### Organization-Scoped Provisioners
**Organization-scoped Provisioners** can pick up build jobs created by any user.
These provisioners always have the implicit tags `scope=organization owner=""`.
```shell
coder provisionerd start --org <organization_name>
```sh
coder provisioner start --psk <your-psk>
```
If you omit the `--org` argument, the provisioner will be assigned to the
default organization.
</div>
```shell
coder provisionerd start
```
### User-scoped Provisioners
**User-scoped Provisioners** can only pick up build jobs created from
user-tagged templates. Unlike the other provisioner types, any Coder user can
run user provisioners, but they have no impact unless there exists at least one
template with the `scope=user` provisioner tag.
```shell
coder provisionerd start \
--tag scope=user
# In another terminal, create/push
# a template that requires user provisioners
coder templates push on-prem \
--provisioner-tag scope=user
```
### Provisioner Tags
## Provisioner Tags
You can use **provisioner tags** to control which provisioners can pick up build
jobs from templates (and corresponding workspaces) with matching explicit tags.
@@ -110,10 +129,10 @@ automatically.
For example:
```shell
```sh
# Start a provisioner with the explicit tags
# environment=on_prem and datacenter=chicago
coder provisionerd start \
coder provisioner start \
--tag environment=on_prem \
--tag datacenter=chicago
@@ -129,6 +148,10 @@ coder templates push on-prem-chicago \
--provisioner-tag datacenter=chicago
```
Alternatively, a template can target a provisioner via
[workspace tags](https://github.com/coder/coder/tree/main/examples/workspace-tags)
inside the Terraform.
A provisioner can run a given build job if one of the below is true:
1. A job with no explicit tags can only be run on a provisioner with no explicit
@@ -176,9 +199,59 @@ This is illustrated in the below table:
> copy the output:
>
> ```
> go test -v -count=1 ./coderd/provisionerdserver/ -test.run='^TestAcquirer_MatchTags/GenTable$'
> go test -v -count=1 ./coderd/provisionerserver/ -test.run='^TestAcquirer_MatchTags/GenTable$'
> ```
## Types of provisioners
Provisioners can broadly be categorized by scope: `organization` or `user`. The
scope of a provisioner can be specified with
[`-tag=scope=<scope>`](../reference/cli/provisioner_start.md#t---tag) when
starting the provisioner daemon. Only users with at least the
[Template Admin](../admin/users.md#roles) role or higher may create
organization-scoped provisioner daemons.
There are two exceptions:
- [Built-in provisioners](../reference/cli/server.md#provisioner-daemons) are
always organization-scoped.
- External provisioners started using a
[pre-shared key (PSK)](../reference/cli/provisioner_start.md#psk) are always
organization-scoped.
### Organization-Scoped Provisioners
**Organization-scoped Provisioners** can pick up build jobs created by any user.
These provisioners always have the implicit tags `scope=organization owner=""`.
```sh
coder provisioner start --org <organization_name>
```
If you omit the `--org` argument, the provisioner will be assigned to the
default organization.
```sh
coder provisioner start
```
### User-scoped Provisioners
**User-scoped Provisioners** can only pick up build jobs created from
user-tagged templates. Unlike the other provisioner types, any Coder user can
run user provisioners, but they have no impact unless there exists at least one
template with the `scope=user` provisioner tag.
```sh
coder provisioner start \
--tag scope=user
# In another terminal, create/push
# a template that requires user provisioners
coder templates push on-prem \
--provisioner-tag scope=user
```
## Example: Running an external provisioner with Helm
Coder provides a Helm chart for running external provisioner daemons, which you
@@ -187,21 +260,21 @@ will use in concert with the Helm chart for deploying the Coder server.
1. Create a long, random pre-shared key (PSK) and store it in a Kubernetes
secret
```shell
```sh
kubectl create secret generic coder-provisioner-psk --from-literal=psk=`head /dev/urandom | base64 | tr -dc A-Za-z0-9 | head -c 26`
```
1. Modify your Coder `values.yaml` to include
```yaml
provisionerDaemon:
provisioneraemon:
pskSecretName: "coder-provisioner-psk"
```
1. Redeploy Coder with the new `values.yaml` to roll out the PSK. You can omit
`--version <your version>` to also upgrade Coder to the latest version.
```shell
```sh
helm upgrade coder coder-v2/coder \
--namespace coder \
--version <your version> \
@@ -217,7 +290,7 @@ will use in concert with the Helm chart for deploying the Coder server.
- name: CODER_URL
value: "https://coder.example.com"
replicaCount: 10
provisionerDaemon:
provisioneraemon:
pskSecretName: "coder-provisioner-psk"
tags:
location: auh
@@ -235,7 +308,7 @@ will use in concert with the Helm chart for deploying the Coder server.
1. Install the provisioner daemon chart
```shell
```sh
helm install coder-provisioner coder-v2/coder-provisioner \
--namespace coder \
--version <your version> \
@@ -244,26 +317,26 @@ will use in concert with the Helm chart for deploying the Coder server.
You can verify that your provisioner daemons have successfully connected to
Coderd by looking for a debug log message that says
`provisionerd: successfully connected to coderd` from each Pod.
`provisioner: successfully connected to coderd` from each Pod.
## Example: Running an external provisioner on a VM
```shell
```sh
curl -L https://coder.com/install.sh | sh
export CODER_URL=https://coder.example.com
export CODER_SESSION_TOKEN=your_token
coder provisionerd start
coder provisioner start
```
## Example: Running an external provisioner via Docker
```shell
```sh
docker run --rm -it \
-e CODER_URL=https://coder.example.com/ \
-e CODER_SESSION_TOKEN=your_token \
--entrypoint /opt/coder \
ghcr.io/coder/coder:latest \
provisionerd start
provisioner start
```
## Disable built-in provisioners
@@ -272,7 +345,7 @@ As mentioned above, the Coder server will run built-in provisioners by default.
This can be disabled with a server-wide
[flag or environment variable](../reference/cli/server.md#provisioner-daemons).
```shell
```sh
coder server --provisioner-daemons=0
```
-1
View File
@@ -102,5 +102,4 @@ Form will never get held up by quota enforcement.
## Up next
- [Enterprise](../enterprise.md)
- [Configuring](./configure.md)
+2 -2
View File
@@ -19,5 +19,5 @@ You can set the following permissions:
## Enabling this feature
This feature is only available with an enterprise license.
[Learn more](../enterprise.md)
This feature is only available with an
[Enterprise or Premium license](https://coder.com/pricing).
-4
View File
@@ -53,7 +53,3 @@ from Winget.
```pwsh
winget install Coder.Coder
```
## Up Next
- [Learn how to enable Enterprise features](../enterprise.md).
+11 -1
View File
@@ -10,7 +10,7 @@ Coder offers these user roles in the community edition:
| | Auditor | User Admin | Template Admin | Owner |
| ----------------------------------------------------- | ------- | ---------- | -------------- | ----- |
| Add and remove Users | | ✅ | | ✅ |
| Manage groups (enterprise) | | ✅ | | ✅ |
| Manage groups (premium) | | ✅ | | ✅ |
| Change User roles | | | | ✅ |
| Manage **ALL** Templates | | | ✅ | ✅ |
| View **ALL** Workspaces | | | ✅ | ✅ |
@@ -22,6 +22,16 @@ Coder offers these user roles in the community edition:
A user may have one or more roles. All users have an implicit Member role that
may use personal workspaces.
## Custom Roles (Premium) (Beta)
Coder v2.16+ deployments can configure custom roles on the
[Organization](./organizations.md) level.
![Custom roles](../images/admin/organizations/custom-roles.png)
> Note: This requires a Premium license.
> [Contact your account team](https://coder.com/contact) for more details.
## Security notes
A malicious Template Admin could write a template that executes commands on the
+1 -1
View File
@@ -64,7 +64,7 @@ ben@coder.com!
Stream Kubernetes event logs to the Coder agent logs to reveal Kuernetes-level
issues such as ResourceQuota limitations, invalid images, etc.
![Kubernetes quota](https://raw.githubusercontent.com/coder/coder/main/docs/platforms/kubernetes/coder-logstream-kube-logs-quota-exceeded.png)
- [OIDC Role Sync](https://coder.com/docs/admin/auth#group-sync-enterprise)
- [OIDC Role Sync](https://coder.com/docs/admin/auth#group-sync-enterprise-premium)
(Enterprise): Sync roles from your OIDC provider to Coder roles (e.g.
`Template Admin`) (#8595) (@Emyrk)
- Users can convert their accounts from username/password authentication to SSO
+33 -9
View File
@@ -1,21 +1,34 @@
# Feature stages
Some Coder features are released as Alpha or Experimental.
Some Coder features are released in feature stages before they are generally
available.
## Alpha features
If you encounter an issue with any Coder feature, please submit a
[GitHub issues](https://github.com/coder/coder/issues) or join the
[Coder Discord](https://discord.gg/coder).
Alpha features are enabled in all Coder deployments but the feature is subject
to change, or even be removed. Breaking changes may not be documented in the
changelog. In most cases, features will only stay in alpha for 1 month.
## Early access features
We recommend using [GitHub issues](https://github.com/coder/coder/issues) to
leave feedback and get support for alpha features.
Early access features are neither feature-complete nor stable. We do not
recommend using early access features in production deployments.
Coder releases early access features behind an “unsafe” experiment, where
theyre accessible but not easy to find.
## Experimental features
These features are disabled by default, and not recommended for use in
production as they may cause performance or stability issues. In most cases,
features will only stay in experimental for 1-2 weeks of internal testing.
experimental features are complete, but require further internal testing and
will stay in the experimental stage for one month.
Coder may make significant changes to experiments or revert features to a
feature flag at any time.
If you plan to activate an experimental feature, we suggest that you use a
staging deployment.
You can opt-out of an experiment after you've enabled it.
```yaml
# Enable all experimental features
@@ -27,7 +40,7 @@ coder server --experiments=feature1,feature2
# Alternatively, use the `CODER_EXPERIMENTS` environment variable.
```
## Available experimental features
### Available experimental features
<!-- Code generated by scripts/release/docs_update_experiments.sh. DO NOT EDIT. -->
<!-- BEGIN: available-experimental-features -->
@@ -37,3 +50,14 @@ coder server --experiments=feature1,feature2
| `notifications` | Sends notifications via SMTP and webhooks following certain events. | mainline, stable |
<!-- END: available-experimental-features -->
## Beta
Beta features are open to the public, but are tagged with a `Beta` label.
Theyre subject to minor changes and may contain bugs, but are generally ready
for use.
## General Availability (GA)
All other features have been tested, are stable, and are enabled by default.
-56
View File
@@ -1,56 +0,0 @@
# Enterprise Features
Coder is free to use and includes some features that are only accessible with a
paid license. [Contact Sales](https://coder.com/contact) for pricing or
[get a free trial](https://coder.com/trial).
| Category | Feature | Open Source | Enterprise |
| --------------- | --------------------------------------------------------------------------------------------------- | :---------: | :--------: |
| Support | Email, Prioritization | ❌ | ✅ |
| Scale | [High Availability](./admin/high-availability.md) | ❌ | ✅ |
| Scale | [Multiple External Auth Providers](./admin/external-auth.md#multiple-external-providers-enterprise) | ❌ | ✅ |
| Scale | [Isolated Terraform Runners](./admin/provisioners.md) | ❌ | ✅ |
| Scale | [Workspace Proxies](./admin/workspace-proxies.md) | ❌ | ✅ |
| Governance | [Audit Logging](./admin/audit-logs.md) | ❌ | ✅ |
| Governance | [Browser Only Connections](./networking/#browser-only-connections-enterprise) | ❌ | ✅ |
| Governance | [Groups & Template RBAC](./admin/rbac.md) | ❌ | ✅ |
| Cost Control | [Quotas](./admin/quotas.md) | ❌ | ✅ |
| Cost Control | [Max Workspace Lifetime](./workspaces.md#max-lifetime) | ❌ | ✅ |
| User Management | [Groups](./admin/groups.md) | ❌ | ✅ |
| User Management | [Group & role sync](./admin/auth.md#group-sync-enterprise) | ❌ | ✅ |
| User Management | [SCIM](./admin/auth.md#scim) | ❌ | ✅ |
## Adding your license key
There are two ways to add an enterprise license to a Coder deployment: In the
Coder UI or with the Coder CLI.
### Coder UI
Click Deployment, Licenses, Add a license then drag or select the license file
with the `jwt` extension.
![Add License UI](./images/add-license-ui.png)
### Coder CLI
### Requirements
- Your license key
- Coder CLI installed
### Instructions
1. Save your license key to disk and make note of the path
2. Open a terminal
3. Ensure you are logged into your Coder deployment
`coder login <access url>`
4. Run
`coder licenses add -f <path to your license key>`
## Up Next
- [Learn how to contribute to Coder](./CONTRIBUTING.md).
+4 -5
View File
@@ -1,14 +1,13 @@
# FAQs
Frequently asked questions on Coder OSS and Enterprise deployments. These FAQs
come from our community and enterprise customers, feel free to
Frequently asked questions on Coder OSS and Premium deployments. These FAQs come
from our community and enterprise customers, feel free to
[contribute to this page](https://github.com/coder/coder/edit/main/docs/faqs.md).
### How do I add an enterprise license?
### How do I add a Premium trial license?
Visit https://coder.com/trial or contact
[sales@coder.com](mailto:sales@coder.com?subject=License) to get a v2 enterprise
trial key.
[sales@coder.com](mailto:sales@coder.com?subject=License) to get a trial key.
You can add a license through the UI or CLI.
+1 -1
View File
@@ -46,7 +46,7 @@ be sent.
Configure Coder to use these claims for group sync. These claims are present in
the `id_token`. See all configuration options for group sync in the
[docs](https://coder.com/docs/admin/auth#group-sync-enterprise).
[docs](https://coder.com/docs/admin/auth#group-sync-enterprise-premium).
```bash
# Add the 'groups' scope.
+1 -1
View File
@@ -1,7 +1,7 @@
# Guides and Tutorials
Here you can find a list of employee-written guides on Coder for OSS and
Enterprise. These tutorials are hosted on our
Premium. These tutorials are hosted on our
[Github](https://github.com/coder/coder/) where you can leave feedback or
request new topics to be covered.
-135
View File
@@ -1,135 +0,0 @@
# Using Organizations
> Note: Organizations is still under active development and requires a
> non-standard enterprise license to use. Do not use organizations on your
> production instance!
>
> For more details, [contact your account team](https://coder.com/contact).
Organizations allow you to run a Coder deployment with multiple platform teams,
all with uniquely scoped templates, provisioners, users, groups, and workspaces.
## Prerequisites
- Coder deployment with non-standard license with Organizations enabled
([contact your account team](https://coder.com/contact))
- User with `Owner` role
- Coder CLI installed on local machine
## Switch to the preview image and enable the experiment
To try the latest organizations features, switch to a preview image in your Helm
chart and enable the
[experimental flag](../reference/cli/server.md#--experiments).
For example, with Kubernetes, set the following in your `values.yaml`:
```yaml
coderd:
image:
repo: ghcr.io/coder/coder-preview
tag: orgs-preview-aug-16
env:
- name: CODER_EXPERIMENTS
value: multi-organization
```
> See all
> [preview images](https://github.com/coder/coder/pkgs/container/coder-preview)
> in GitHub. Preview images prefixed with `main-` expire after a week.
Then, upgrade your deployment:
```sh
helm upgrade coder coder-v2/coder -f values.yaml
```
## The default organization
All Coder deployments start with one organization called `Default`.
To edit the organization details, navigate to `Deployment -> Organizations` in
the top bar:
![Organizations Menu](../images/guides/using-organizations/deployment-organizations.png)
From there, you can manage the name, icon, description, users, and groups:
![Organization Settings](../images/guides/using-organizations/default-organization.png)
## Guide: Your first organization
### 1. Create the organization
Within the sidebar, click `New organization` to create an organization. In this
example, we'll create the `data-platform` org.
![New Organization](../images/guides/using-organizations/new-organization.png)
From there, let's deploy a provisioner and template for this organization.
### 2. Deploy a provisioner
[Provisioners](../admin/provisioners.md) are organization-scoped and are
responsible for executing Terraform/OpenTofu to provision the infrastructure for
workspaces and testing templates. Before creating templates, we must deploy at
least one provisioner as the built-in provisioners are scoped to the default
organization.
using Coder CLI, run the following command to create a key that will be used to
authenticate the provisioner:
```sh
coder provisioner keys create data-cluster-key --org data-platform
Successfully created provisioner key data-cluster! Save this authentication token, it will not be shown again.
< key omitted >
```
Next, start the provisioner with the key on your desired platform. In this
example, we'll start it using the Coder CLI on a host with Docker. For
instructions on using other platforms like Kubernetes, see our
[provisioner documentation](../admin/provisioners.md).
```sh
export CODER_URL=https://<your-coder-url>
export CODER_PROVISIONER_DAEMON_KEY=<key>
coder provisionerd start --org <org-name>
```
### 3. Create a template
Once you've started a provisioner, you can create a template. You'll notice the
"Create Template" screen now has an organization dropdown:
![Template Org Picker](../images/guides/using-organizations/template-org-picker.png)
### 5. Add members
Navigate to `Deployment->Organizations` to add members to your organization.
Once added, they will be able to see the organization-specific templates.
![Add members](../images/guides/using-organizations/organization-members.png)
### 6. Create a workspace
Now, users in the data platform organization will see the templates related to
their organization. Users can be in multiple organizations.
![Workspace List](../images/guides/using-organizations/workspace-list.png)
## Planned work
Organizations is under active development. The work is planned before
organizations is generally available:
- View provisioner health via the Coder UI
- Custom Role support in Coder UI
- Per-organization quotas
- Improved visibility of organization-specific resources throughout the UI
- Sync OIDC claims to auto-assign users to organizations / roles + SCIM support
## Support & Feedback
[Contact your account team](https://coder.com/contact) if you have any questions
or feedback.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 20V3.75L5 0L10 3.75V6H20V20H0ZM2 18H4V16H2V18ZM2 14H4V12H2V14ZM2 10H4V8H2V10ZM2 6H4V4H2V6ZM6 6H8V4H6V6ZM6 18H18V8H6V18ZM12 12V10H16V12H12ZM12 16V14H16V16H12ZM8 12V10H10V12H8ZM8 16V14H10V16H8Z" fill="#E8EAED"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 20V3.75L5 0L10 3.75V6H20V20H0ZM2 18H4V16H2V18ZM2 14H4V12H2V14ZM2 10H4V8H2V10ZM2 6H4V4H2V6ZM6 6H8V4H6V6ZM6 18H18V8H6V18ZM12 12V10H16V12H12ZM12 16V14H16V16H12ZM8 12V10H10V12H8ZM8 16V14H10V16H8Z" fill="#E8EAED"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

+47
View File
@@ -0,0 +1,47 @@
# Licensing
Some features are only accessible with a Premium or Enterprise license. See our
[pricing page](https://coder.com/pricing) for more details. To try Premium
features, you can [request a trial](https://coder.com/trial) or
[contact sales](https://coder.com/contact).
<!-- markdown-link-check-disable -->
> If you are an existing customer, you can learn more our new Premium plan in
> the [Coder v2.16 blog post](https://coder.com/blog/release-recap-2-16-0)
<!-- markdown-link-check-enable -->
## Adding your license key
There are two ways to add a license to a Coder deployment:
<div class="tabs">
### Coder UI
First, ensure you have a license key
([request a trial](https://coder.com/trial)).
With an `Owner` account, navigate to `Deployment -> Licenses`, `Add a license`
then drag or select the license file with the `jwt` extension.
![Add License UI](./images/add-license-ui.png)
### Coder CLI
First, ensure you have a license key
([request a trial](https://coder.com/trial)) and the
[Coder CLI](./install/index.md) installed.
1. Save your license key to disk and make note of the path
2. Open a terminal
3. Ensure you are logged into your Coder deployment
`coder login <access url>`
4. Run
`coder licenses add -f <path to your license key>`
</div>
+25 -23
View File
@@ -282,7 +282,7 @@
"title": "Process Logging",
"description": "Audit commands in workspaces with exectrace",
"path": "./templates/process-logging.md",
"state": "enterprise"
"state": ["enterprise", "premium"]
},
{
"title": "Icons",
@@ -393,14 +393,21 @@
"description": "Learn how to manage user groups",
"path": "./admin/groups.md",
"icon_path": "./images/icons/group.svg",
"state": "enterprise"
"state": ["enterprise", "premium"]
},
{
"title": "RBAC",
"description": "Learn how to use the role based access control",
"title": "Organizations",
"description": "Learn how to manage organizations",
"path": "./admin/organizations.md",
"icon_path": "./images/icons/orgs.svg",
"state": ["premium"]
},
{
"title": "Template RBAC",
"description": "Learn how to use the role based access control against templates",
"path": "./admin/rbac.md",
"icon_path": "./images/icons/rbac.svg",
"state": "enterprise"
"state": ["enterprise", "beta"]
},
{
"title": "Configuration",
@@ -443,14 +450,14 @@
"description": "Run provisioners isolated from the Coder server",
"path": "./admin/provisioners.md",
"icon_path": "./images/icons/queue.svg",
"state": "enterprise"
"state": ["enterprise", "premium"]
},
{
"title": "Workspace Proxies",
"description": "Run geo distributed workspace proxies",
"path": "./admin/workspace-proxies.md",
"icon_path": "./images/icons/networking.svg",
"state": "enterprise"
"state": ["enterprise", "premium"]
},
{
"title": "Application Logs",
@@ -463,21 +470,21 @@
"description": "Learn how to use Audit Logs in your Coder deployment",
"path": "./admin/audit-logs.md",
"icon_path": "./images/icons/radar.svg",
"state": "enterprise"
"state": ["enterprise", "premium"]
},
{
"title": "Quotas",
"description": "Learn how to use Workspace Quotas in Coder",
"path": "./admin/quotas.md",
"icon_path": "./images/icons/dollar.svg",
"state": "enterprise"
"state": ["enterprise", "premium"]
},
{
"title": "High Availability",
"description": "Learn how to configure Coder for High Availability",
"path": "./admin/high-availability.md",
"icon_path": "./images/icons/hydra.svg",
"state": "enterprise"
"state": ["enterprise", "premium"]
},
{
"title": "Prometheus",
@@ -490,7 +497,7 @@
"description": "Learn how to configure the appearance of Coder",
"path": "./admin/appearance.md",
"icon_path": "./images/icons/info.svg",
"state": "enterprise"
"state": ["enterprise", "premium"]
},
{
"title": "Telemetry",
@@ -503,7 +510,7 @@
"description": "Learn how to encrypt sensitive data at rest in Coder",
"path": "./admin/encryption.md",
"icon_path": "./images/icons/lock.svg",
"state": "enterprise"
"state": ["enterprise", "premium"]
},
{
"title": "Deployment Health",
@@ -521,23 +528,23 @@
"title": "Slack Notifications",
"description": "Learn how to setup Slack notifications",
"path": "./admin/notifications/slack.md",
"state": "beta"
"state": ["beta"]
},
{
"title": "Microsoft Teams Notifications",
"description": "Learn how to setup Microsoft Teams notifications",
"path": "./admin/notifications/teams.md",
"state": "beta"
"state": ["beta"]
}
]
}
]
},
{
"title": "Enterprise",
"description": "Learn how to enable Enterprise features",
"path": "./enterprise.md",
"icon_path": "./images/icons/group.svg"
"title": "Licensing",
"description": "Learn how to enable Premium features",
"path": "./licensing.md",
"icon_path": "./images/icons/licensing.svg"
},
{
"title": "Contributing",
@@ -1337,11 +1344,6 @@
"title": "Cloning Git Repositories",
"description": "Automatically clone Git repositories into your workspace",
"path": "./guides/cloning-git-repositories.md"
},
{
"title": "Using Organizations",
"description": "Learn how to use our (early access) Organizations functionality",
"path": "./guides/using-organizations.md"
}
]
}
+3 -3
View File
@@ -157,10 +157,10 @@ $ coder server --derp-config-path derpmap.json
The dashboard (and web apps opened through the dashboard) are served from the
coder server, so they can only be geo-distributed with High Availability mode in
our Enterprise Edition. [Reach out to Sales](https://coder.com/contact) to learn
more.
our Enterprise and Premium Editions.
[Reach out to Sales](https://coder.com/contact) to learn more.
## Browser-only connections (enterprise)
## Browser-only connections (enterprise) (premium)
Some Coder deployments require that all access is through the browser to comply
with security policies. In these cases, pass the `--browser-only` flag to
+1 -1
View File
@@ -129,7 +129,7 @@ resource uses a different method of authentication and **is not impacted by the
template's maximum sharing level**, nor the level of a shared port that points
to the app.
### Configure maximum port sharing level (enterprise)
### Configure maximum port sharing level (enterprise) (premium)
Enterprise-licensed template admins can control the maximum port sharing level
for workspaces under a given template in the template settings. By default, the
+2 -2
View File
@@ -83,7 +83,7 @@ coder_app.
![Autostop UI](./images/autostop.png)
### Autostop requirement (enterprise)
### Autostop requirement (enterprise) (premium)
Autostop requirement is a template setting that determines how often workspaces
using the template must automatically stop. Autostop requirement ignores any
@@ -113,7 +113,7 @@ Autostop requirement is disabled when the template is using the deprecated max
lifetime feature. Templates can choose to use a max lifetime or an autostop
requirement during the deprecation period, but only one can be used at a time.
### User quiet hours (enterprise)
### User quiet hours (enterprise) (premium)
User quiet hours can be configured in the user's schedule settings page.
Workspaces on templates with an autostop requirement will only be forcibly
-1
View File
@@ -448,7 +448,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
// with the below route, we need to register this route without any mounts or groups to make both work.
r.With(
apiKeyMiddleware,
httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentNotifications),
httpmw.ExtractNotificationTemplateParam(options.Database),
).Put("/notifications/templates/{notification_template}/method", api.updateNotificationTemplateMethod)
})
-1
View File
@@ -23,7 +23,6 @@ func createOpts(t *testing.T) *coderdenttest.Options {
t.Helper()
dt := coderdtest.DeploymentValues(t)
dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
return &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dt,
+6 -2
View File
@@ -147,9 +147,13 @@ func (api *API) provisionerKeyDaemons(rw http.ResponseWriter, r *http.Request) {
pkDaemons := []codersdk.ProvisionerKeyDaemons{}
for _, key := range sdkKeys {
// currently we exclude user-auth from this list
// The key.OrganizationID for the `user-auth` key is hardcoded to
// the default org in the database and we are overwriting it here
// to be the correct org we used to query the list.
// This will be changed when we update the `user-auth` keys to be
// directly tied to a user ID.
if key.ID.String() == codersdk.ProvisionerKeyIDUserAuth {
continue
key.OrganizationID = organization.ID
}
daemons := []codersdk.ProvisionerDaemon{}
for _, daemon := range recentDaemons {
+5 -5
View File
@@ -174,15 +174,15 @@ require (
go.uber.org/atomic v1.11.0
go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29
go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516
golang.org/x/crypto v0.27.0
golang.org/x/crypto v0.31.0
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
golang.org/x/mod v0.21.0
golang.org/x/net v0.29.0
golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.8.0
golang.org/x/sys v0.25.0
golang.org/x/term v0.24.0
golang.org/x/text v0.18.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0
golang.org/x/term v0.27.0
golang.org/x/text v0.21.0
golang.org/x/tools v0.25.0
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
google.golang.org/api v0.197.0
+10 -10
View File
@@ -1058,8 +1058,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
@@ -1108,8 +1108,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1151,8 +1151,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1161,8 +1161,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1174,8 +1174,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+6 -6
View File
@@ -169,12 +169,12 @@ func writeDocs(sections [][]byte) error {
// Update manifest.json
type route struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Path string `json:"path,omitempty"`
IconPath string `json:"icon_path,omitempty"`
State string `json:"state,omitempty"`
Children []route `json:"children,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Path string `json:"path,omitempty"`
IconPath string `json:"icon_path,omitempty"`
State []string `json:"state,omitempty"`
Children []route `json:"children,omitempty"`
}
type manifest struct {
+6 -6
View File
@@ -14,12 +14,12 @@ import (
// route is an individual page object in the docs manifest.json.
type route struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Path string `json:"path,omitempty"`
IconPath string `json:"icon_path,omitempty"`
State string `json:"state,omitempty"`
Children []route `json:"children,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Path string `json:"path,omitempty"`
IconPath string `json:"icon_path,omitempty"`
State []string `json:"state,omitempty"`
Children []route `json:"children,omitempty"`
}
// manifest describes the entire documentation index.
@@ -10,7 +10,7 @@ import { docs } from "utils/docs";
* All types of feature that we are currently supporting. Defined as record to
* ensure that we can't accidentally make typos when writing the badge text.
*/
const featureStageBadgeTypes = {
export const featureStageBadgeTypes = {
beta: "beta",
experimental: "experimental",
} as const satisfies Record<string, ReactNode>;
+1 -1
View File
@@ -58,7 +58,7 @@ export const Paywall: FC<PaywallProps> = ({
</ul>
<div css={styles.learnButton}>
<Button
href={docs("/enterprise")}
href={docs("/licensing")}
target="_blank"
rel="noreferrer"
startIcon={<span css={{ fontSize: 22 }}>&rarr;</span>}
@@ -62,7 +62,7 @@ export const PopoverPaywall: FC<PopoverPaywallProps> = ({
</ul>
<div css={styles.learnButton}>
<Button
href={docs("/enterprise")}
href={docs("/licensing")}
target="_blank"
rel="noreferrer"
startIcon={<span css={{ fontSize: 22 }}>&rarr;</span>}
@@ -27,7 +27,7 @@ import { createDayString } from "utils/createDayString";
import { docs } from "utils/docs";
import { ProvisionerTag } from "./ProvisionerTag";
type ProvisionerGroupType = "builtin" | "psk" | "key";
type ProvisionerGroupType = "builtin" | "userAuth" | "psk" | "key";
interface ProvisionerGroupProps {
readonly buildInfo: BuildInfoResponse;
@@ -103,7 +103,8 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
: `${provisionersWithWarnings} provisioners`;
const hasMultipleTagVariants =
type === "psk" && provisioners.some((it) => !isSimpleTagSet(it.tags));
(type === "psk" || type === "userAuth") &&
provisioners.some((it) => !isSimpleTagSet(it.tags));
return (
<div
@@ -143,6 +144,8 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
</>
)}
{type === "userAuth" && <UserAuthProvisionerTitle />}
{type === "psk" && <PskProvisionerTitle />}
{type === "key" && (
<h4 css={styles.groupTitle}>Key group &ndash; {keyName}</h4>
@@ -249,7 +252,7 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
</span>
</div>
{hasMultipleTagVariants && (
<PskProvisionerTags tags={provisioner.tags} />
<InlineProvisionerTags tags={provisioner.tags} />
)}
</Stack>
</div>
@@ -335,11 +338,11 @@ const ProvisionerVersionPopover: FC<ProvisionerVersionPopoverProps> = ({
);
};
interface PskProvisionerTagsProps {
interface InlineProvisionerTagsProps {
tags: Record<string, string>;
}
const PskProvisionerTags: FC<PskProvisionerTagsProps> = ({ tags }) => {
const InlineProvisionerTags: FC<InlineProvisionerTagsProps> = ({ tags }) => {
const daemonScope = tags.scope || "organization";
const iconScope =
daemonScope === "organization" ? <BusinessIcon /> : <PersonIcon />;
@@ -413,6 +416,30 @@ const BuiltinProvisionerTitle: FC = () => {
);
};
const UserAuthProvisionerTitle: FC = () => {
return (
<h4 css={styles.groupTitle}>
<Stack direction="row" alignItems="end" spacing={1}>
<span>User-authenticated provisioners</span>
<HelpTooltip>
<HelpTooltipTrigger />
<HelpTooltipContent>
<HelpTooltipTitle>User-authenticated provisioners</HelpTooltipTitle>
<HelpTooltipText>
These provisioners are connected by users using the{" "}
<code>coder</code> CLI, and are authorized by the users
credentials. They can be tagged to only run provisioner jobs for
that user. User-authenticated provisioners are only available for
the default organization.{" "}
<Link href={docs("/")}>Learn more&hellip;</Link>
</HelpTooltipText>
</HelpTooltipContent>
</HelpTooltip>
</Stack>
</h4>
);
};
const PskProvisionerTitle: FC = () => {
return (
<h4 css={styles.groupTitle}>
@@ -43,6 +43,7 @@ export const NotificationsPage: FC = () => {
title="Notifications"
description="Control delivery methods for notifications on this deployment."
layout="fluid"
featureStage={"beta"}
>
<Tabs active={tab}>
<TabsList>
@@ -7,6 +7,7 @@ import NotificationsIcon from "@mui/icons-material/NotificationsNoneOutlined";
import Globe from "@mui/icons-material/PublicOutlined";
import ApprovalIcon from "@mui/icons-material/VerifiedUserOutlined";
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { GitIcon } from "components/Icons/GitIcon";
import {
Sidebar as BaseSidebar,
@@ -51,11 +52,9 @@ export const Sidebar: FC = () => {
<SidebarNavItem href="observability" icon={InsertChartIcon}>
Observability
</SidebarNavItem>
{experiments.includes("notifications") && (
<SidebarNavItem href="notifications" icon={NotificationsIcon}>
Notifications
</SidebarNavItem>
)}
<SidebarNavItem href="notifications" icon={NotificationsIcon}>
Notifications <FeatureStageBadge contentType="beta" size="sm" />
</SidebarNavItem>
</BaseSidebar>
);
};
+28 -35
View File
@@ -28,6 +28,15 @@ export const LoginPage: FC = () => {
const navigate = useNavigate();
const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
let redirectError: Error | null = null;
let redirectUrl: URL | null = null;
try {
redirectUrl = new URL(redirectTo);
} catch {
// Do nothing
}
const isApiRouteRedirect = redirectTo.startsWith("/api/v2");
useEffect(() => {
if (!buildInfoQuery.data || isSignedIn) {
@@ -42,41 +51,24 @@ export const LoginPage: FC = () => {
}, [isSignedIn, buildInfoQuery.data, user?.id]);
if (isSignedIn) {
if (buildInfoQuery.data) {
// This uses `navigator.sendBeacon`, so window.href
// will not stop the request from being sent!
sendDeploymentEvent(buildInfoQuery.data, {
type: "deployment_login",
user_id: user?.id,
});
// The reason we need `window.location.href` for api redirects is that
// we need the page to reload and make a request to the backend. If we
// use `<Navigate>`, react would handle the redirect itself and never
// request the page from the backend.
if (isApiRouteRedirect) {
const sanitizedUrl = new URL(redirectTo, window.location.origin);
window.location.href = sanitizedUrl.pathname + sanitizedUrl.search;
// Setting the href should immediately request a new page. Show an
// error state if it doesn't.
redirectError = new Error("unable to redirect");
} else {
return (
<Navigate
to={redirectUrl ? redirectUrl.pathname : redirectTo}
replace
/>
);
}
// If the redirect is going to a workspace application, and we
// are missing authentication, then we need to change the href location
// to trigger a HTTP request. This allows the BE to generate the auth
// cookie required. Similarly for the OAuth2 exchange as the authorization
// page is served by the backend.
// If no redirect is present, then ignore this branched logic.
if (redirectTo !== "" && redirectTo !== "/") {
try {
// This catches any absolute redirects. Relative redirects
// will fail the try/catch. Subdomain apps are absolute redirects.
const redirectURL = new URL(redirectTo);
if (redirectURL.host !== window.location.host) {
window.location.href = redirectTo;
return null;
}
} catch {
// Do nothing
}
// Path based apps and OAuth2.
if (redirectTo.includes("/apps/") || redirectTo.includes("/oauth2/")) {
window.location.href = redirectTo;
return null;
}
}
return <Navigate to={redirectTo} replace />;
}
if (isConfiguringTheFirstUser) {
@@ -90,7 +82,7 @@ export const LoginPage: FC = () => {
</Helmet>
<LoginPageView
authMethods={authMethodsQuery.data}
error={signInError}
error={signInError ?? redirectError}
isLoading={isLoading || authMethodsQuery.isLoading}
buildInfo={buildInfoQuery.data}
isSigningIn={isSigningIn}
@@ -98,6 +90,7 @@ export const LoginPage: FC = () => {
await signIn(email, password);
navigate("/");
}}
redirectTo={redirectTo}
/>
</>
);
+2 -2
View File
@@ -6,7 +6,6 @@ import { Loader } from "components/Loader/Loader";
import { type FC, useState } from "react";
import { useLocation } from "react-router-dom";
import { getApplicationName, getLogoURL } from "utils/appearance";
import { retrieveRedirect } from "utils/redirect";
import { SignInForm } from "./SignInForm";
import { TermsOfServiceLink } from "./TermsOfServiceLink";
@@ -17,6 +16,7 @@ export interface LoginPageViewProps {
buildInfo?: BuildInfoResponse;
isSigningIn: boolean;
onSignIn: (credentials: { email: string; password: string }) => void;
redirectTo: string;
}
export const LoginPageView: FC<LoginPageViewProps> = ({
@@ -26,9 +26,9 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
buildInfo,
isSigningIn,
onSignIn,
redirectTo,
}) => {
const location = useLocation();
const redirectTo = retrieveRedirect(location.search);
// This allows messages to be displayed at the top of the sign in form.
// Helpful for any redirects that want to inform the user of something.
const message = new URLSearchParams(location.search).get("message");
@@ -69,7 +69,7 @@ export const CreateOrganizationPageView: FC<
<div>
<SettingsHeader
title="New Organization"
description="Organize your deployment into multiple platform teams."
description="Organize your deployment into multiple platform teams with unique provisioners, templates, groups, and members."
/>
{Boolean(error) && !isApiValidationError(error) && (
@@ -92,7 +92,7 @@ export const CreateOrganizationPageView: FC<
<PopoverPaywall
message="Organizations"
description="Create multiple organizations within a single Coder deployment, allowing several platform teams to operate with isolated users, templates, and distinct underlying infrastructure."
documentationLink={docs("/guides/using-organizations")}
documentationLink={docs("/admin/organizations")}
/>
</PopoverContent>
</Popover>
@@ -104,7 +104,7 @@ export const CreateOrganizationPageView: FC<
<Paywall
message="Organizations"
description="Create multiple organizations within a single Coder deployment, allowing several platform teams to operate with isolated users, templates, and distinct underlying infrastructure."
documentationLink={docs("/guides/using-organizations")}
documentationLink={docs("/admin/organizations")}
/>
</Cond>
<Cond>
@@ -21,7 +21,9 @@ export const IdpSyncHelpTooltip: FC = () => {
Coder. Use the Coder CLI to configure these mappings.
</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipLink href={docs("/admin/auth#group-sync-enterprise")}>
<HelpTooltipLink
href={docs("/admin/auth#group-sync-enterprise-premium")}
>
Configure IdP Sync
</HelpTooltipLink>
</HelpTooltipLinksGroup>
@@ -74,7 +74,7 @@ export const IdpSyncPage: FC = () => {
<Button
startIcon={<LaunchOutlined />}
component="a"
href={docs("/admin/auth#group-sync-enterprise")}
href={docs("/admin/auth#group-sync-enterprise-premium")}
target="_blank"
>
Setup IdP Sync
@@ -85,7 +85,9 @@ export const IdpSyncPage: FC = () => {
<Paywall
message="IdP Sync"
description="Configure group and role mappings to manage permissions outside of Coder. You need an Premium license to use this feature."
documentationLink={docs("/admin/groups")}
documentationLink={docs(
"/admin/auth#group-sync-enterprise-premium",
)}
/>
</Cond>
<Cond>
@@ -313,7 +313,7 @@ const IdpMappingTable: FC<IdpMappingTableProps> = ({
startIcon={<LaunchOutlined />}
component="a"
href={docs(
`/admin/auth#${type.toLowerCase()}-sync-enterprise`,
`/admin/auth#${type.toLowerCase()}-sync-enterprise-premium`,
)}
target="_blank"
>
@@ -403,7 +403,7 @@ const LegacyGroupSyncHeader: FC = () => {
configure IdP sync via the CLI, which enables sync to be
configured for any organization, and for those settings to be
persisted without manually setting environment variables.{" "}
<Link href={docs("/admin/auth#group-sync-enterprise")}>
<Link href={docs("/admin/auth#group-sync-enterprise-premium")}>
Learn more&hellip;
</Link>
</HelpTooltipText>
@@ -7,6 +7,7 @@ import {
MockProvisionerBuiltinKey,
MockProvisionerKey,
MockProvisionerPskKey,
MockProvisionerUserAuthKey,
MockProvisionerWithTags,
MockUserProvisioner,
mockApiError,
@@ -79,6 +80,17 @@ export const Provisioners: Story = {
name: `ケイラ-${i}`,
})),
},
{
key: MockProvisionerUserAuthKey,
daemons: [
MockUserProvisioner,
{
...MockUserProvisioner,
id: "mock-user-provisioner-2",
name: "Test User Provisioner 2",
},
],
},
],
},
play: async ({ step }) => {
@@ -92,7 +92,7 @@ const ViewContent: FC<ViewContentProps> = ({ buildInfo, provisioners }) => {
target="_blank"
href={docs("/admin/provisioners")}
>
Show me how to create a provisioner
Create a provisioner
</Button>
}
/>
@@ -110,28 +110,16 @@ const ViewContent: FC<ViewContentProps> = ({ buildInfo, provisioners }) => {
</div>
)}
<Stack spacing={4.5}>
{provisioners.map((group) => {
const type = getGroupType(group.key);
// We intentionally hide user-authenticated provisioners for now
// because there are 1. some grouping issues on the backend and 2. we
// should ideally group them by the user who authenticated them, and
// not just lump them all together.
if (type === "userAuth") {
return null;
}
return (
<ProvisionerGroup
key={group.key.id}
buildInfo={buildInfo}
keyName={group.key.name}
keyTags={group.key.tags}
type={type}
provisioners={group.daemons}
/>
);
})}
{provisioners.map((group) => (
<ProvisionerGroup
key={group.key.id}
buildInfo={buildInfo}
keyName={group.key.name}
keyTags={group.key.tags}
type={getGroupType(group.key)}
provisioners={group.daemons}
/>
))}
</Stack>
</>
);
@@ -148,11 +148,12 @@ const DeploymentSettingsNavigation: FC<DeploymentSettingsNavigationProps> = ({
Users
</SidebarNavSubItem>
)}
{experiments.includes("notifications") && (
<Stack direction="row" alignItems="center" css={{ gap: 0 }}>
<SidebarNavSubItem href="notifications">
Notifications
</SidebarNavSubItem>
)}
<FeatureStageBadge contentType="beta" size="sm" />
</Stack>
</Stack>
)}
</div>
+1 -1
View File
@@ -211,7 +211,7 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
quotas, and more.
</span>
<Link
href={docs("/enterprise")}
href={docs("/licensing")}
target="_blank"
css={{ marginTop: 4, display: "inline-block", fontSize: 13 }}
>
@@ -99,6 +99,7 @@ export const NotificationsPage: FC = () => {
title="Notifications"
description="Configure your notification preferences. Icons on the right of each notification indicate delivery method, either SMTP or Webhook."
layout="fluid"
featureStage="beta"
>
{ready ? (
<Stack spacing={4}>
+26 -10
View File
@@ -1,4 +1,9 @@
import type { Interpolation, Theme } from "@emotion/react";
import {
FeatureStageBadge,
type featureStageBadgeTypes,
} from "components/FeatureStageBadge/FeatureStageBadge";
import { Stack } from "components/Stack/Stack";
import type { FC, ReactNode } from "react";
type SectionLayout = "fixed" | "fluid";
@@ -13,6 +18,7 @@ export interface SectionProps {
layout?: SectionLayout;
className?: string;
children?: ReactNode;
featureStage?: keyof typeof featureStageBadgeTypes;
}
export const Section: FC<SectionProps> = ({
@@ -24,6 +30,7 @@ export const Section: FC<SectionProps> = ({
className = "",
children,
layout = "fixed",
featureStage,
}) => {
return (
<section className={className} id={id} data-testid={id}>
@@ -32,16 +39,25 @@ export const Section: FC<SectionProps> = ({
<div css={styles.header}>
<div>
{title && (
<h4
css={{
fontSize: 24,
fontWeight: 500,
margin: 0,
marginBottom: 8,
}}
>
{title}
</h4>
<Stack direction={"row"} alignItems="center">
<h4
css={{
fontSize: 24,
fontWeight: 500,
margin: 0,
marginBottom: 8,
}}
>
{title}
</h4>
{featureStage && (
<FeatureStageBadge
contentType={featureStage}
size="lg"
css={{ marginBottom: "5px" }}
/>
)}
</Stack>
)}
{description && typeof description === "string" && (
<p css={styles.description}>{description}</p>
+4 -5
View File
@@ -6,6 +6,7 @@ import NotificationsIcon from "@mui/icons-material/NotificationsNoneOutlined";
import AccountIcon from "@mui/icons-material/Person";
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined";
import type { User } from "api/typesGenerated";
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import { GitIcon } from "components/Icons/GitIcon";
import {
Sidebar as BaseSidebar,
@@ -57,11 +58,9 @@ export const Sidebar: FC<SidebarProps> = ({ user }) => {
<SidebarNavItem href="tokens" icon={VpnKeyOutlined}>
Tokens
</SidebarNavItem>
{experiments.includes("notifications") && (
<SidebarNavItem href="notifications" icon={NotificationsIcon}>
Notifications
</SidebarNavItem>
)}
<SidebarNavItem href="notifications" icon={NotificationsIcon}>
Notifications <FeatureStageBadge contentType="beta" size="sm" />
</SidebarNavItem>
</BaseSidebar>
);
};
+1 -1
View File
@@ -610,7 +610,7 @@ export const MockProvisioner2: TypesGen.ProvisionerDaemon = {
};
export const MockUserProvisioner: TypesGen.ProvisionerDaemon = {
...MockProvisioner,
...MockUserAuthProvisioner,
id: "test-user-provisioner",
name: "Test User Provisioner",
tags: { scope: "user", owner: "12345678-abcd-1234-abcd-1234567890abcd" },
+1
View File
@@ -35,6 +35,7 @@
"dotfiles.svg",
"dotnet.svg",
"fedora.svg",
"filebrowser.svg",
"fleet.svg",
"fly.io.svg",
"folder.svg",
+147
View File
@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xml:space="preserve"
width="560"
height="560"
version="1.1"
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
viewBox="0 0 560 560"
id="svg44"
sodipodi:docname="icon_raw.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
inkscape:export-filename="/home/umarcor/filebrowser/logo/icon_raw.svg.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><metadata
id="metadata48"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="711"
id="namedview46"
showgrid="false"
inkscape:zoom="0.33714286"
inkscape:cx="-172.33051"
inkscape:cy="280"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="svg44" />
<defs
id="defs4">
<style
type="text/css"
id="style2">
<![CDATA[
.fil1 {fill:#FEFEFE}
.fil6 {fill:#006498}
.fil7 {fill:#0EA5EB}
.fil8 {fill:#2979FF}
.fil3 {fill:#2BBCFF}
.fil0 {fill:#455A64}
.fil4 {fill:#53C6FC}
.fil5 {fill:#BDEAFF}
.fil2 {fill:#332C2B;fill-opacity:0.149020}
]]>
</style>
</defs>
<g
id="g85"
transform="translate(-70,-70)"><path
class="fil1"
d="M 350,71 C 504,71 629,196 629,350 629,504 504,629 350,629 196,629 71,504 71,350 71,196 196,71 350,71 Z"
id="path9"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil2"
d="M 475,236 593,387 C 596,503 444,639 301,585 L 225,486 339,330 c 0,0 138,-95 136,-94 z"
id="path11"
inkscape:connector-curvature="0"
style="fill:#332c2b;fill-opacity:0.14902003" /><path
class="fil3"
d="m 231,211 h 208 l 38,24 v 246 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 V 219 c 0,-5 3,-8 8,-8 z"
id="path13"
inkscape:connector-curvature="0"
style="fill:#2bbcff" /><path
class="fil4"
d="m 231,211 h 208 l 38,24 v 2 L 440,214 H 231 c -4,0 -7,3 -7,7 v 263 c -1,-1 -1,-2 -1,-3 V 219 c 0,-5 3,-8 8,-8 z"
id="path15"
inkscape:connector-curvature="0"
style="fill:#53c6fc" /><polygon
class="fil5"
points="305,212 418,212 418,310 305,310 "
id="polygon17"
style="fill:#bdeaff" /><path
class="fil5"
d="m 255,363 h 189 c 3,0 5,2 5,4 V 483 H 250 V 367 c 0,-2 2,-4 5,-4 z"
id="path19"
inkscape:connector-curvature="0"
style="fill:#bdeaff" /><polygon
class="fil6"
points="250,470 449,470 449,483 250,483 "
id="polygon21"
style="fill:#006498" /><path
class="fil6"
d="m 380,226 h 10 c 3,0 6,2 6,5 v 40 c 0,3 -3,6 -6,6 h -10 c -3,0 -6,-3 -6,-6 v -40 c 0,-3 3,-5 6,-5 z"
id="path23"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil1"
d="m 254,226 c 10,0 17,7 17,17 0,9 -7,16 -17,16 -9,0 -17,-7 -17,-16 0,-10 8,-17 17,-17 z"
id="path25"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil6"
d="m 267,448 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,3 -3,3 H 267 c -2,0 -3,-2 -3,-3 v 0 c 0,-2 1,-3 3,-3 z"
id="path27"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil6"
d="m 267,415 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,2 -3,2 H 267 c -2,0 -3,-1 -3,-2 v 0 c 0,-2 1,-3 3,-3 z"
id="path29"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil6"
d="m 267,381 h 165 c 2,0 3,2 3,3 v 0 c 0,2 -1,3 -3,3 H 267 c -2,0 -3,-1 -3,-3 v 0 c 0,-1 1,-3 3,-3 z"
id="path31"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil1"
d="m 236,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
id="path33"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil1"
d="m 463,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
id="path35"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><polygon
class="fil6"
points="305,212 284,212 284,310 305,310 "
id="polygon37"
style="fill:#006498" /><path
class="fil7"
d="m 477,479 v 2 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 v -2 c 0,4 3,8 8,8 h 238 c 5,0 8,-4 8,-8 z"
id="path39"
inkscape:connector-curvature="0"
style="fill:#0ea5eb" /><path
class="fil8"
d="M 350,70 C 505,70 630,195 630,350 630,505 505,630 350,630 195,630 70,505 70,350 70,195 195,70 350,70 Z m 0,46 C 479,116 584,221 584,350 584,479 479,584 350,584 221,584 116,479 116,350 116,221 221,116 350,116 Z"
id="path41"
inkscape:connector-curvature="0"
style="fill:#2979ff" /></g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB