che-operator/pkg/controller/che/oauth_initial_htpasswd_prov...

273 lines
8.1 KiB
Go

//
// Copyright (c) 2012-2021 Red Hat, Inc.
// This program and the accompanying materials are made
// available under the terms of the Eclipse Public License 2.0
// which is available at https://www.eclipse.org/legal/epl-2.0/
//
// SPDX-License-Identifier: EPL-2.0
//
// Contributors:
// Red Hat, Inc. - initial API and implementation
//
package che
import (
"context"
errorMsg "errors"
"github.com/eclipse-che/che-operator/pkg/deploy"
"github.com/eclipse-che/che-operator/pkg/util"
configv1 "github.com/openshift/api/config/v1"
oauthv1 "github.com/openshift/api/config/v1"
userv1 "github.com/openshift/api/user/v1"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
htpasswdIdentityProviderName = "htpasswd-eclipse-che"
htpasswdSecretName = "htpasswd-eclipse-che"
ocConfigNamespace = "openshift-config"
openShiftOAuthUserCredentialsSecret = "openshift-oauth-user-credentials"
)
var (
password = util.GeneratePasswd(6)
htpasswdFileContent string
)
// OpenShiftOAuthUserHandler - handler to create or delete new Openshift oAuth user.
type OpenShiftOAuthUserHandler interface {
SyncOAuthInitialUser(openshiftOAuth *oauthv1.OAuth, deployContext *deploy.DeployContext) (bool, error)
DeleteOAuthInitialUser(deployContext *deploy.DeployContext) error
}
// OpenShiftOAuthUserOperatorHandler - OpenShiftOAuthUserHandler implementation.
type OpenShiftOAuthUserOperatorHandler struct {
OpenShiftOAuthUserHandler
runtimeClient client.Client
runnable util.Runnable
}
// NewOpenShiftOAuthUserHandler - create new OpenShiftOAuthUserHandler instance
func NewOpenShiftOAuthUserHandler(runtimeClient client.Client) OpenShiftOAuthUserHandler {
return &OpenShiftOAuthUserOperatorHandler{
runtimeClient: runtimeClient,
// real process implementation. In the test we are using mock.
runnable: util.NewRunnable(),
}
}
// SyncOAuthInitialUser - creates new htpasswd provider with inital user with Che flavor name
// if Openshift cluster hasn't got identity providers, otherwise do nothing.
// It usefull for good first user expirience.
// User can't use kube:admin or system:admin user in the Openshift oAuth. That's why we provide
// initial user for good first meeting with Eclipse Che.
func (iuh *OpenShiftOAuthUserOperatorHandler) SyncOAuthInitialUser(openshiftOAuth *oauthv1.OAuth, deployContext *deploy.DeployContext) (bool, error) {
cr := deployContext.CheCluster
userName := deploy.DefaultCheFlavor(cr)
if htpasswdFileContent == "" {
var err error
if htpasswdFileContent, err = iuh.generateHtPasswdUserInfo(userName, password); err != nil {
return false, err
}
}
initialUserSecretData := map[string][]byte{"user": []byte(userName), "password": []byte(password)}
done, err := deploy.SyncSecretToCluster(deployContext, openShiftOAuthUserCredentialsSecret, cr.Namespace, initialUserSecretData)
if !done {
return false, err
}
credentionalSecret := &corev1.Secret{}
exists, err := deploy.GetNamespacedObject(deployContext, openShiftOAuthUserCredentialsSecret, credentionalSecret)
if !exists {
return false, err
}
storedPassword := string(credentionalSecret.Data["password"])
if password != storedPassword {
password = storedPassword
if htpasswdFileContent, err = iuh.generateHtPasswdUserInfo(userName, password); err != nil {
return false, err
}
}
htpasswdFileSecretData := map[string][]byte{"htpasswd": []byte(htpasswdFileContent)}
done, err = deploy.SyncSecretToCluster(deployContext, htpasswdSecretName, ocConfigNamespace, htpasswdFileSecretData)
if !done {
return false, err
}
if err := appendIdentityProvider(openshiftOAuth, iuh.runtimeClient); err != nil {
return false, err
}
return true, nil
}
// DeleteOAuthInitialUser - removes initial user, htpasswd provider, htpasswd secret and Che secret with username and password.
func (iuh *OpenShiftOAuthUserOperatorHandler) DeleteOAuthInitialUser(deployContext *deploy.DeployContext) error {
oAuth, err := GetOpenshiftOAuth(iuh.runtimeClient)
if err != nil {
return err
}
cr := deployContext.CheCluster
userName := deploy.DefaultCheFlavor(cr)
if err := deleteUser(iuh.runtimeClient, userName); err != nil {
return err
}
if err := deleteUserIdentity(iuh.runtimeClient, userName); err != nil {
return err
}
if err := deleteIdentityProvider(oAuth, iuh.runtimeClient); err != nil {
return err
}
_, err = deploy.Delete(deployContext, types.NamespacedName{Name: htpasswdSecretName, Namespace: ocConfigNamespace}, &corev1.Secret{})
if err != nil {
return err
}
_, err = deploy.DeleteNamespacedObject(deployContext, openShiftOAuthUserCredentialsSecret, &corev1.Secret{})
if err != nil {
return err
}
return nil
}
func (iuh *OpenShiftOAuthUserOperatorHandler) generateHtPasswdUserInfo(userName string, password string) (string, error) {
logrus.Info("Generate initial user httpasswd info")
err := iuh.runnable.Run("htpasswd", "-nbB", userName, password)
if err != nil {
return "", err
}
if len(iuh.runnable.GetStdErr()) > 0 {
return "", errorMsg.New("Failed to generate data for HTPasswd identity provider: " + iuh.runnable.GetStdErr())
}
return iuh.runnable.GetStdOut(), nil
}
// GetOpenshiftOAuth returns Openshift oAuth object.
func GetOpenshiftOAuth(runtimeClient client.Client) (*oauthv1.OAuth, error) {
oAuth := &oauthv1.OAuth{}
if err := runtimeClient.Get(context.TODO(), types.NamespacedName{Name: "cluster"}, oAuth); err != nil {
return nil, err
}
return oAuth, nil
}
func identityProviderExists(providerName string, oAuth *oauthv1.OAuth) bool {
if len(oAuth.Spec.IdentityProviders) == 0 {
return false
}
for _, identityProvider := range oAuth.Spec.IdentityProviders {
if identityProvider.Name == providerName {
return true
}
}
return false
}
func appendIdentityProvider(oAuth *oauthv1.OAuth, runtimeClient client.Client) error {
logrus.Info("Add initial user httpasswd provider to the oAuth")
htpasswdProvider := newHtpasswdProvider()
if !identityProviderExists(htpasswdProvider.Name, oAuth) {
oauthPatch := client.MergeFrom(oAuth.DeepCopy())
oAuth.Spec.IdentityProviders = append(oAuth.Spec.IdentityProviders, *htpasswdProvider)
if err := runtimeClient.Patch(context.TODO(), oAuth, oauthPatch); err != nil {
return err
}
}
return nil
}
func newHtpasswdProvider() *oauthv1.IdentityProvider {
return &oauthv1.IdentityProvider{
Name: htpasswdIdentityProviderName,
MappingMethod: configv1.MappingMethodClaim,
IdentityProviderConfig: oauthv1.IdentityProviderConfig{
Type: "HTPasswd",
HTPasswd: &oauthv1.HTPasswdIdentityProvider{
FileData: oauthv1.SecretNameReference{Name: htpasswdSecretName},
},
},
}
}
func deleteUser(runtimeClient client.Client, userName string) error {
logrus.Infof("Delete initial user: %s", userName)
user := &userv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: userName,
},
}
if err := runtimeClient.Delete(context.TODO(), user); err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
return nil
}
func deleteUserIdentity(runtimeClient client.Client, userName string) error {
identityName := htpasswdIdentityProviderName + ":" + userName
logrus.Infof("Delete initial user identity: %s", identityName)
identity := &userv1.Identity{
ObjectMeta: metav1.ObjectMeta{
Name: identityName,
},
}
if err := runtimeClient.Delete(context.TODO(), identity); err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
return nil
}
func deleteIdentityProvider(oAuth *configv1.OAuth, runtimeClient client.Client) error {
logrus.Info("Delete initial user httpasswd provider from the oAuth")
oauthPatch := client.MergeFrom(oAuth.DeepCopy())
ips := oAuth.Spec.IdentityProviders
for i, ip := range ips {
if ip.Name == htpasswdIdentityProviderName {
// remove provider from slice
oAuth.Spec.IdentityProviders = append(ips[:i], ips[i+1:]...)
break
}
}
if err := runtimeClient.Patch(context.TODO(), oAuth, oauthPatch); err != nil {
return err
}
return nil
}