Skip to content

Commit

Permalink
Merge pull request #7095 from jerolimov/odc-4755
Browse files Browse the repository at this point in the history
Add API to create user settings
  • Loading branch information
openshift-merge-robot committed Dec 1, 2020
2 parents c456683 + d023732 commit b54fdd5
Show file tree
Hide file tree
Showing 7 changed files with 512 additions and 7 deletions.
13 changes: 6 additions & 7 deletions cmd/bridge/main.go
Expand Up @@ -543,29 +543,28 @@ func main() {
bridge.FlagFatalf("user-auth", "must be one of: oidc, openshift, disabled")
}

var resourceListerToken string
switch *fK8sAuth {
case "service-account":
bridge.ValidateFlagIs("k8s-mode", *fK8sMode, "in-cluster")
srv.StaticUser = &auth.User{
Token: k8sAuthServiceAccountBearerToken,
}
resourceListerToken = k8sAuthServiceAccountBearerToken
srv.ServiceAccountToken = k8sAuthServiceAccountBearerToken
case "bearer-token":
bridge.ValidateFlagNotEmpty("k8s-auth-bearer-token", *fK8sAuthBearerToken)
srv.StaticUser = &auth.User{
Token: *fK8sAuthBearerToken,
}
resourceListerToken = *fK8sAuthBearerToken
srv.ServiceAccountToken = *fK8sAuthBearerToken
case "oidc", "openshift":
bridge.ValidateFlagIs("user-auth", *fUserAuth, "oidc", "openshift")
resourceListerToken = k8sAuthServiceAccountBearerToken
srv.ServiceAccountToken = k8sAuthServiceAccountBearerToken
default:
bridge.FlagFatalf("k8s-mode", "must be one of: service-account, bearer-token, oidc, openshift")
}

srv.MonitoringDashboardConfigMapLister = server.NewResourceLister(
resourceListerToken,
srv.ServiceAccountToken,
&url.URL{
Scheme: k8sEndpoint.Scheme,
Host: k8sEndpoint.Host,
Expand All @@ -583,7 +582,7 @@ func main() {
)

srv.KnativeEventSourceCRDLister = server.NewResourceLister(
resourceListerToken,
srv.ServiceAccountToken,
&url.URL{
Scheme: k8sEndpoint.Scheme,
Host: k8sEndpoint.Host,
Expand All @@ -601,7 +600,7 @@ func main() {
)

srv.KnativeChannelCRDLister = server.NewResourceLister(
resourceListerToken,
srv.ServiceAccountToken,
&url.URL{
Scheme: k8sEndpoint.Scheme,
Host: k8sEndpoint.Host,
Expand Down
12 changes: 12 additions & 0 deletions pkg/server/server.go
Expand Up @@ -23,6 +23,8 @@ import (
"github.com/openshift/console/pkg/proxy"
"github.com/openshift/console/pkg/serverutils"
"github.com/openshift/console/pkg/terminal"

"github.com/openshift/console/pkg/usersettings"
"github.com/openshift/console/pkg/version"

graphql "github.com/graph-gophers/graphql-go"
Expand Down Expand Up @@ -96,6 +98,7 @@ type Server struct {
TectonicVersion string
Auther *auth.Authenticator
StaticUser *auth.User
ServiceAccountToken string
KubectlClientID string
KubeAPIServerURL string
DocumentationBaseURL *url.URL
Expand Down Expand Up @@ -389,6 +392,15 @@ func (s *Server) HTTPHandler() http.Handler {
handle("/api/console/knative-channels", authHandler(s.handleKnativeChannelCRDs))
handle("/api/console/version", authHandler(s.versionHandler))

// User settings
userSettingHandler := usersettings.UserSettingsHandler{
K8sProxyConfig: s.K8sProxyConfig,
Client: s.K8sClient,
Endpoint: s.K8sProxyConfig.Endpoint.String(),
ServiceAccountToken: s.ServiceAccountToken,
}
handle("/api/console/user-settings", authHandlerWithUser(userSettingHandler.HandleUserSettings))

// Helm Endpoints
helmHandlers := helmhandlerspkg.New(s.K8sProxyConfig.Endpoint.String(), s.K8sClient.Transport)
handle("/api/helm/template", authHandlerWithUser(helmHandlers.HandleHelmRenderManifests))
Expand Down
180 changes: 180 additions & 0 deletions pkg/usersettings/handlers.go
@@ -0,0 +1,180 @@
package usersettings

import (
"context"
"fmt"
"net/http"

core "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog"

"github.com/openshift/console/pkg/auth"
"github.com/openshift/console/pkg/proxy"
"github.com/openshift/console/pkg/serverutils"
)

const namespace = "openshift-console-user-settings"

var USER_RESOURCE = schema.GroupVersionResource{
Group: "user.openshift.io",
Version: "v1",
Resource: "users",
}

type UserSettingsHandler struct {
K8sProxyConfig *proxy.Config
Client *http.Client
Endpoint string
ServiceAccountToken string
}

func (h *UserSettingsHandler) HandleUserSettings(user *auth.User, w http.ResponseWriter, r *http.Request) {
context := context.TODO()

serviceAccountClient, err := h.createServiceAccountClient()
if err != nil {
h.sendErrorResponse("Failed to create service account to handle user setting request: %v", err, w)
return
}

userSettingMeta, err := h.getUserSettingMeta(context, user)
if err != nil {
h.sendErrorResponse("Failed to get user data to handle user setting request: %v", err, w)
return
}

switch r.Method {
case http.MethodGet:
configMap, err := h.getUserSettings(context, serviceAccountClient, userSettingMeta)
if err != nil {
h.sendErrorResponse("Failed to get user settings: %v", err, w)
return
}
serverutils.SendResponse(w, http.StatusOK, configMap)
case http.MethodPost:
configMap, err := h.createUserSettings(context, serviceAccountClient, userSettingMeta)
if err != nil {
h.sendErrorResponse("Failed to create user settings: %v", err, w)
return
}
serverutils.SendResponse(w, http.StatusOK, configMap)
case http.MethodDelete:
err := h.deleteUserSettings(context, serviceAccountClient, userSettingMeta)
if err != nil {
h.sendErrorResponse("Failed to delete user settings: %v", err, w)
return
}
w.WriteHeader(http.StatusNoContent)
default:
w.Header().Set("Allow", "GET, POST, DELETE")
serverutils.SendResponse(w, http.StatusMethodNotAllowed, serverutils.ApiError{Err: "Unsupported method, supported methods are GET POST DELETE"})
}
}

func (h *UserSettingsHandler) sendErrorResponse(format string, err error, w http.ResponseWriter) {
errMsg := fmt.Sprintf(format, err)
klog.Errorf(errMsg)
code := http.StatusBadGateway
if apierrors.IsNotFound(err) {
code = http.StatusNotFound
} else if apierrors.IsForbidden(err) {
code = http.StatusForbidden
}
serverutils.SendResponse(w, code, serverutils.ApiError{Err: errMsg})
}

// Fetch the user-setting ConfigMap of the current user, by using his token.
func (h *UserSettingsHandler) getUserSettings(ctx context.Context, client *kubernetes.Clientset, userSettingMeta *UserSettingMeta) (*core.ConfigMap, error) {
return client.CoreV1().ConfigMaps(namespace).Get(ctx, userSettingMeta.getConfigMapName(), meta.GetOptions{})
}

// Create a new user-setting ConfigMap, incl. Role and RoleBinding for the current user.
// Returns the existing ConfigMap if it is already exist.
func (h *UserSettingsHandler) createUserSettings(ctx context.Context, client *kubernetes.Clientset, userSettingMeta *UserSettingMeta) (*core.ConfigMap, error) {
role := createRole(userSettingMeta)
roleBinding := createRoleBinding(userSettingMeta)
configMap := createConfigMap(userSettingMeta)

_, err := client.RbacV1().Roles(namespace).Create(ctx, role, meta.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
h.deleteUserSettings(ctx, client, userSettingMeta)
return nil, err
}

_, err = client.RbacV1().RoleBindings(namespace).Create(ctx, roleBinding, meta.CreateOptions{})
if err != nil && !apierrors.IsAlreadyExists(err) {
h.deleteUserSettings(ctx, client, userSettingMeta)
return nil, err
}

configMap, err = client.CoreV1().ConfigMaps(namespace).Create(ctx, configMap, meta.CreateOptions{})
if err != nil {
// Return actual ConfigMap if it is already created
if apierrors.IsAlreadyExists(err) {
klog.Infof("User settings ConfigMap \"%s\" already exist, will return existing data.", userSettingMeta.getConfigMapName())
return client.CoreV1().ConfigMaps(namespace).Get(ctx, userSettingMeta.getConfigMapName(), meta.GetOptions{})
}
h.deleteUserSettings(ctx, client, userSettingMeta)
return nil, err
}
return configMap, nil
}

// Deletes the user-setting ConfigMap, Role and RoleBinding of the current user.
// It handles not found responses, so that it does not fail on multiple calls.
func (h *UserSettingsHandler) deleteUserSettings(ctx context.Context, client *kubernetes.Clientset, userSettingMeta *UserSettingMeta) error {
err := client.RbacV1().RoleBindings(namespace).Delete(ctx, userSettingMeta.getRoleBindingName(), meta.DeleteOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return err
}

err = client.RbacV1().Roles(namespace).Delete(ctx, userSettingMeta.getRoleName(), meta.DeleteOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return err
}

err = client.CoreV1().ConfigMaps(namespace).Delete(ctx, userSettingMeta.getConfigMapName(), meta.DeleteOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return err
}

return nil
}

func (h *UserSettingsHandler) createServiceAccountClient() (*kubernetes.Clientset, error) {
config := &rest.Config{
Host: h.Endpoint,
BearerToken: h.ServiceAccountToken,
Transport: h.Client.Transport,
}
return kubernetes.NewForConfig(config)
}

func (h *UserSettingsHandler) createUserProxyClient(user *auth.User) (dynamic.Interface, error) {
config := &rest.Config{
Host: h.Endpoint,
BearerToken: user.Token,
Transport: h.Client.Transport,
}
return dynamic.NewForConfig(config)
}

func (h *UserSettingsHandler) getUserSettingMeta(context context.Context, user *auth.User) (*UserSettingMeta, error) {
client, err := h.createUserProxyClient(user)
if err != nil {
return nil, err
}

userInfo, err := client.Resource(USER_RESOURCE).Get(context, "~", meta.GetOptions{})
if err != nil {
return nil, err
}

return newUserSettingMeta(userInfo.GetName(), string(userInfo.GetUID()))
}
95 changes: 95 additions & 0 deletions pkg/usersettings/helpers.go
@@ -0,0 +1,95 @@
package usersettings

import (
"errors"

core "k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func newUserSettingMeta(name string, uid string) (*UserSettingMeta, error) {
resourceIdentifier := ""

if uid != "" {
resourceIdentifier = uid
} else if name == "kube:admin" {
resourceIdentifier = "kubeadmin"
} else {
return nil, errors.New("User must have UID to get required resource data for user-settings")
}

return &UserSettingMeta{
Username: name,
UID: uid,
ResourceIdentifier: resourceIdentifier,
}, nil
}

func createRole(userSettingMeta *UserSettingMeta) *rbac.Role {
return &rbac.Role{
TypeMeta: meta.TypeMeta{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "Role",
},
ObjectMeta: meta.ObjectMeta{
Name: userSettingMeta.getRoleName(),
},
Rules: []rbac.PolicyRule{
rbac.PolicyRule{
APIGroups: []string{
"", // Core group, not "v1"
},
Resources: []string{
"configmaps", // Not "ConfigMap"
},
Verbs: []string{
"get",
"list",
"patch",
"update",
"watch",
},
ResourceNames: []string{
userSettingMeta.getConfigMapName(),
},
},
},
}
}

func createRoleBinding(userSettingMeta *UserSettingMeta) *rbac.RoleBinding {
return &rbac.RoleBinding{
TypeMeta: meta.TypeMeta{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "RoleBinding",
},
ObjectMeta: meta.ObjectMeta{
Name: userSettingMeta.getRoleBindingName(),
},
Subjects: []rbac.Subject{
rbac.Subject{
APIGroup: "rbac.authorization.k8s.io",
Kind: "User",
Name: userSettingMeta.Username,
},
},
RoleRef: rbac.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: userSettingMeta.getRoleName(),
},
}
}

func createConfigMap(userSettingMeta *UserSettingMeta) *core.ConfigMap {
return &core.ConfigMap{
TypeMeta: meta.TypeMeta{
APIVersion: "v1",
Kind: "ConfigMap",
},
ObjectMeta: meta.ObjectMeta{
Name: userSettingMeta.getConfigMapName(),
},
}
}

0 comments on commit b54fdd5

Please sign in to comment.