Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Installer]: change authProviders from raw data to a secret #7177

Merged
merged 3 commits into from
Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions .werft/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,14 +477,26 @@ export async function deployToDevWithInstaller(deploymentConfig: DeploymentConfi
exec(`yq w -i config.yaml observability.tracing.endpoint ${tracingEndpoint}`, {slice: installerSlices.INSTALLER_RENDER});
}

// TODO: Remove this after #6867 is done
werft.log("authProviders", "copy authProviders")
werft.log("authProviders", "copy authProviders from secret")
try {
exec(`kubectl get secret preview-envs-authproviders --namespace=keys -o yaml \
| yq r - data.authProviders \
exec(`for row in $(kubectl get secret preview-envs-authproviders --namespace=keys -o jsonpath="{.data.authProviders}" \
| base64 -d -w 0 \
> ./authProviders`, { silent: true });
exec(`yq merge --inplace config.yaml ./authProviders`, { silent: true })
| yq r - authProviders -j \
| jq -r 'to_entries | .[] | @base64'); do
key=$(echo $row | base64 -d | jq -r '.key')
providerId=$(echo $row | base64 -d | jq -r '.value.id | ascii_downcase')
data=$(echo $row | base64 -d | yq r - value --prettyPrint)

yq w -i ./config.yaml authProviders[$key].kind "secret"
yq w -i ./config.yaml authProviders[$key].name "$providerId"

kubectl create secret generic "$providerId" \
--namespace "${namespace}" \
--from-literal=provider="$data" \
kylos101 marked this conversation as resolved.
Show resolved Hide resolved
--dry-run=client -o yaml | \
kubectl replace --force -f -
done`, { silent: true })

werft.done('authProviders');
} catch (err) {
werft.fail('authProviders', err);
Expand Down
25 changes: 21 additions & 4 deletions components/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RateLimiterConfig } from './auth/rate-limiter';
import { CodeSyncConfig } from './code-sync/code-sync-service';
import { ChargebeeProviderOptions, readOptionsFromFile } from "@gitpod/gitpod-payment-endpoint/lib/chargebee";
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import { log, LogrusLogLevel } from '@gitpod/gitpod-protocol/lib/util/logging';
import { filePathTelepresenceAware, KubeStage, translateLegacyStagename } from '@gitpod/gitpod-protocol/lib/env';
import { BrandingParser } from './branding-parser';
Expand Down Expand Up @@ -86,6 +87,7 @@ export interface ConfigSerialized {
enableLocalApp: boolean;

authProviderConfigs: AuthProviderParams[];
authProviderConfigFiles: string[];
builtinAuthProvidersConfigured: boolean;
disableDynamicAuthProviderLogin: boolean;

Expand Down Expand Up @@ -170,10 +172,25 @@ export namespace ConfigFile {

function loadAndCompleteConfig(config: ConfigSerialized): Config {
const hostUrl = new GitpodHostUrl(config.hostUrl);
let authProviderConfigs = config.authProviderConfigs
if (authProviderConfigs) {
authProviderConfigs = normalizeAuthProviderParams(authProviderConfigs);
let authProviderConfigs: AuthProviderParams[] = []
const rawProviderConfigs = config.authProviderConfigs
if (rawProviderConfigs) {
/* Add raw provider data */
authProviderConfigs.push(...rawProviderConfigs);
}
const rawProviderConfigFiles = config.authProviderConfigFiles
if (rawProviderConfigFiles) {
/* Add providers from files */
const authProviderConfigFiles: AuthProviderParams[] = rawProviderConfigFiles.map<AuthProviderParams>((providerFile) => {
const rawProviderData = fs.readFileSync(providerFile, "utf-8")

return yaml.load(rawProviderData) as AuthProviderParams
});

authProviderConfigs.push(...authProviderConfigFiles);
}
authProviderConfigs = normalizeAuthProviderParams(authProviderConfigs)

const builtinAuthProvidersConfigured = authProviderConfigs.length > 0;
const chargebeeProviderOptions = readOptionsFromFile(filePathTelepresenceAware(config.chargebeeProviderOptionsFile || ""));
let brandingConfig = config.brandingConfig;
Expand All @@ -183,7 +200,7 @@ export namespace ConfigFile {
let license = config.license
const licenseFile = config.licenseFile
if (licenseFile) {
license = fs.readFileSync(licenseFile, "utf-8")
license = fs.readFileSync(licenseFile, "utf-8");
}
return {
...config,
Expand Down
40 changes: 29 additions & 11 deletions installer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,21 +148,39 @@ is `true`. External dependencies can be used in their place

## Auth Providers

> This may move to a secret in future [#6867](https://github.com/gitpod-io/gitpod/issues/6867)

Gitpod must be connected to a Git provider. This can be done via the
dashboard on first load, or by providing `authProviders` configuration.
dashboard on first load, or by providing `authProviders` configuration
as a Kubernetes secret.

### Setting via config

1. Update your configuration file:

```yaml
authProviders:
- id: Public-GitHub
host: github.com
type: GitHub
oauth:
clientId: xxx
clientSecret: xxx
callBackUrl: https://$DOMAIN/auth/github.com/callback
settingsUrl: xxx
- kind: secret
name: public-github
```

2. Create a secret file:

```yaml
# Save this public-github.yaml

id: Public-GitHub
host: github.com
type: GitHub
oauth:
clientId: xxx
clientSecret: xxx
callBackUrl: https://$DOMAIN/auth/github.com/callback
settingsUrl: xxx
```

3. Create the secret:

```shell
kubectl create secret generic --from-file=provider=./public-github.yaml public-github
```

## In-cluster vs External Dependencies
Expand Down
12 changes: 10 additions & 2 deletions installer/pkg/components/server/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,16 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
MinAgeDays: 14,
MinAgePrebuildDays: 7,
},
EnableLocalApp: true,
AuthProviderConfigs: ctx.Config.AuthProviders,
EnableLocalApp: true,
AuthProviderConfigFiles: func() []string {
providers := make([]string, 0)

for _, provider := range ctx.Config.AuthProviders {
providers = append(providers, fmt.Sprintf("%s/%s", authProviderFilePath, provider.Name))
}

return providers
}(),
BuiltinAuthProvidersConfigured: len(ctx.Config.AuthProviders) > 0,
DisableDynamicAuthProviderLogin: false,
BrandingConfig: BrandingConfig{
Expand Down
15 changes: 8 additions & 7 deletions installer/pkg/components/server/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
)

const (
Component = common.ServerComponent
ContainerPort = 3000
ContainerPortName = "http"
licenseFilePath = "/gitpod/license"
PrometheusPort = 9500
PrometheusPortName = "metrics"
ServicePort = 3000
Component = common.ServerComponent
ContainerPort = 3000
ContainerPortName = "http"
authProviderFilePath = "/gitpod/auth-providers"
licenseFilePath = "/gitpod/license"
PrometheusPort = 9500
PrometheusPortName = "metrics"
ServicePort = 3000
)
21 changes: 21 additions & 0 deletions installer/pkg/components/server/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
})
}

if len(ctx.Config.AuthProviders) > 0 {
for i, provider := range ctx.Config.AuthProviders {
volumeName := fmt.Sprintf("auth-provider-%d", i)
volumes = append(volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: provider.Name,
},
},
})

volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: volumeName,
MountPath: fmt.Sprintf("%s/%s", authProviderFilePath, provider.Name),
SubPath: "provider",
ReadOnly: true,
})
}
}

return []runtime.Object{
&appsv1.Deployment{
TypeMeta: common.TypeMetaDeployment,
Expand Down
24 changes: 12 additions & 12 deletions installer/pkg/components/server/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ type ConfigSerialized struct {
ChargebeeProviderOptionsFile string `json:"chargebeeProviderOptionsFile"`
EnablePayment bool `json:"enablePayment"`

WorkspaceHeartbeat WorkspaceHeartbeat `json:"workspaceHeartbeat"`
WorkspaceDefaults WorkspaceDefaults `json:"workspaceDefaults"`
Session Session `json:"session"`
GitHubApp GitHubApp `json:"githubApp"`
WorkspaceGarbageCollection WorkspaceGarbageCollection `json:"workspaceGarbageCollection"`
AuthProviderConfigs []config.AuthProviderConfigs `json:"authProviderConfigs"`
BrandingConfig BrandingConfig `json:"brandingConfig"`
IncrementalPrebuilds IncrementalPrebuilds `json:"incrementalPrebuilds"`
BlockNewUsers config.BlockNewUsers `json:"blockNewUsers"`
OAuthServer OAuthServer `json:"oauthServer"`
RateLimiter RateLimiter `json:"rateLimiter"`
CodeSync CodeSync `json:"codeSync"`
WorkspaceHeartbeat WorkspaceHeartbeat `json:"workspaceHeartbeat"`
WorkspaceDefaults WorkspaceDefaults `json:"workspaceDefaults"`
Session Session `json:"session"`
GitHubApp GitHubApp `json:"githubApp"`
WorkspaceGarbageCollection WorkspaceGarbageCollection `json:"workspaceGarbageCollection"`
AuthProviderConfigFiles []string `json:"authProviderConfigFiles"`
kylos101 marked this conversation as resolved.
Show resolved Hide resolved
BrandingConfig BrandingConfig `json:"brandingConfig"`
IncrementalPrebuilds IncrementalPrebuilds `json:"incrementalPrebuilds"`
BlockNewUsers config.BlockNewUsers `json:"blockNewUsers"`
OAuthServer OAuthServer `json:"oauthServer"`
RateLimiter RateLimiter `json:"rateLimiter"`
CodeSync CodeSync `json:"codeSync"`
}

type CodeSyncResources struct {
Expand Down
48 changes: 16 additions & 32 deletions installer/pkg/config/v1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type version struct{}

func (v version) Factory() interface{} {
return &Config{
AuthProviders: []AuthProviderConfigs{},
AuthProviders: []ObjectRef{},
BlockNewUsers: BlockNewUsers{
Enabled: false,
Passlist: []string{},
Expand Down Expand Up @@ -81,9 +81,9 @@ type Config struct {

Workspace Workspace `json:"workspace" validate:"required"`

AuthProviders []AuthProviderConfigs `json:"authProviders" validate:"dive"`
BlockNewUsers BlockNewUsers `json:"blockNewUsers"`
License *ObjectRef `json:"license,omitempty"`
AuthProviders []ObjectRef `json:"authProviders" validate:"dive"`
BlockNewUsers BlockNewUsers `json:"blockNewUsers"`
License *ObjectRef `json:"license,omitempty"`
}

type Metadata struct {
Expand Down Expand Up @@ -229,38 +229,22 @@ const (
FSShiftShiftFS FSShiftMethod = "shiftfs"
)

// todo(sje): I don't know if we want to put this in the config YAML
type AuthProviderConfigs struct {
ID string `json:"id" validate:"required"`
Host string `json:"host" validate:"required"`
Type string `json:"type" validate:"required"`
BuiltIn string `json:"builtin"`
Verified string `json:"verified"`
OAuth OAuth `json:"oauth" validate:"required"`
Params map[string]string `json:"params"`
HiddenOnDashboard bool `json:"hiddenOnDashboard"`
LoginContextMatcher string `json:"loginContextMatcher"`
DisallowLogin bool `json:"disallowLogin"`
RequireTOS bool `json:"requireTOS"`
Description string `json:"description"`
Icon string `json:"icon"`
}

type BlockNewUsers struct {
Enabled bool `json:"enabled"`
Passlist []string `json:"passlist"`
}

// AuthProviderConfigs this only contains what is necessary for validation
type AuthProviderConfigs struct {
ID string `json:"id" validate:"required"`
Host string `json:"host" validate:"required"`
Type string `json:"type" validate:"required"`
OAuth OAuth `json:"oauth" validate:"required"`
}

// OAuth this only contains what is necessary for validation
type OAuth struct {
ClientId string `json:"clientId" validate:"required"`
ClientSecret string `json:"clientSecret" validate:"required"`
CallBackUrl string `json:"callBackUrl" validate:"required"`
AuthorizationUrl string `json:"authorizationUrl"`
TokenUrl string `json:"tokenUrl"`
Scope string `json:"scope"`
ScopeSeparator string `json:"scopeSeparator"`
SettingsUrl string `json:"settingsUrl"`
AuthorizationParams map[string]string `json:"authorizationParams"`
ConfigURL string `json:"configURL"`
ConfigFn string `json:"configFn"`
ClientId string `json:"clientId" validate:"required"`
ClientSecret string `json:"clientSecret" validate:"required"`
CallBackUrl string `json:"callBackUrl" validate:"required"`
}
43 changes: 43 additions & 0 deletions installer/pkg/config/v1/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
package config

import (
"fmt"

"github.com/gitpod-io/gitpod/installer/pkg/cluster"
"sigs.k8s.io/yaml"

"github.com/go-playground/validator/v10"
corev1 "k8s.io/api/core/v1"
)

var InstallationKindList = map[InstallationKind]struct{}{
Expand Down Expand Up @@ -107,5 +111,44 @@ func (v version) ClusterValidation(rcfg interface{}) cluster.ValidationChecks {
res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("license")))
}

if len(cfg.AuthProviders) > 0 {
for _, provider := range cfg.AuthProviders {
secretName := provider.Name
secretKey := "provider"
res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData(secretKey), cluster.CheckSecretRule(func(s *corev1.Secret) ([]cluster.ValidationError, error) {
errors := make([]cluster.ValidationError, 0)
providerData := s.Data[secretKey]

var provider AuthProviderConfigs
err := yaml.Unmarshal(providerData, &provider)
if err != nil {
return nil, err
}

validate := validator.New()
err = v.LoadValidationFuncs(validate)
if err != nil {
return nil, err
}

err = validate.Struct(provider)
if err != nil {
validationErrors := err.(validator.ValidationErrors)

if len(validationErrors) > 0 {
for _, v := range validationErrors {
errors = append(errors, cluster.ValidationError{
Message: fmt.Sprintf("Field '%s' failed %s validation", v.Namespace(), v.Tag()),
Type: cluster.ValidationStatusError,
})
}
}
}

return errors, nil
})))
}
}

return res
}