Skip to content

Commit

Permalink
feat: implement CLI commands for user management
Browse files Browse the repository at this point in the history
Add the UI for showing and editing service accounts.
Fixes: #197

Signed-off-by: Artem Chernyshev <[email protected]>
  • Loading branch information
Unix4ever committed Dec 11, 2024
1 parent bbbf6f2 commit b1ab823
Show file tree
Hide file tree
Showing 25 changed files with 1,023 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .kres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ spec:
environment:
WITH_DEBUG: "true"
INTEGRATION_RUN_E2E_TEST: "true"
INTEGRATION_TEST_ARGS: "--test.run CleanState/|Auth/|DefaultCluster/"
INTEGRATION_TEST_ARGS: "--test.run CleanState/|Auth/|DefaultCluster/|CLICommands/"
TALEMU_TEST_ARGS: "--test.run ImmediateClusterDestruction/|EncryptedCluster/|SinglenodeCluster/|ScaleUpAndDown/|ScaleUpAndDownMachineClassBasedMachineSets/|TalosUpgrades/|KubernetesUpgrades/|MaintenanceDowngrade/|ClusterTemplate/|ScaleUpAndDownAutoProvisionMachineSets/"
RUN_TALEMU_TESTS: true
jobs:
Expand Down
4 changes: 3 additions & 1 deletion client/pkg/access/serviceaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
)

const (
serviceAccountDomain = "serviceaccount.omni.sidero.dev"
// tsgen:ServiceAccountDomain
serviceAccountDomain = "serviceaccount.omni.sidero.dev"
// tsgen:InfraProviderServiceAccountDomain
infraProviderServiceAccountDomain = "infra-provider." + serviceAccountDomain

// ServiceAccountNameSuffix is appended to the name of all service accounts.
Expand Down
1 change: 1 addition & 0 deletions client/pkg/omni/resources/auth/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (

const (
// LabelPublicKeyUserID is the label that defines the user ID of the public key.
// tsgen:LabelPublicKeyUserID
LabelPublicKeyUserID = "user-id"

// LabelIdentityUserID is a label linking identity to the user.
Expand Down
11 changes: 11 additions & 0 deletions client/pkg/omnictl/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package omnictl

import "github.com/siderolabs/omni/client/pkg/omnictl/user"

func init() {
RootCmd.AddCommand(user.RootCmd())
}
72 changes: 72 additions & 0 deletions client/pkg/omnictl/user/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package user

import (
"context"
"fmt"

"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/google/uuid"
"github.com/spf13/cobra"

"github.com/siderolabs/omni/client/pkg/client"
"github.com/siderolabs/omni/client/pkg/omni/resources"
"github.com/siderolabs/omni/client/pkg/omni/resources/auth"
"github.com/siderolabs/omni/client/pkg/omnictl/internal/access"
)

var createCmdFlags struct {
role string
}

// createCmd represents the user create command.
var createCmd = &cobra.Command{
Use: "create [email]",
Short: "Create a user.",
Long: `Create a user with the specified email.`,
Example: "",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
return access.WithClient(createUser(args[0]))
},
}

func createUser(email string) func(ctx context.Context, client *client.Client) error {
return func(ctx context.Context, client *client.Client) error {
user := auth.NewUser(resources.DefaultNamespace, uuid.NewString())

user.TypedSpec().Value.Role = createCmdFlags.role

identity := auth.NewIdentity(resources.DefaultNamespace, email)

identity.Metadata().Labels().Set(auth.LabelIdentityUserID, user.Metadata().ID())

identity.TypedSpec().Value.UserId = user.Metadata().ID()

existing, err := safe.ReaderGetByID[*auth.Identity](ctx, client.Omni().State(), email)
if err != nil && !state.IsNotFoundError(err) {
return err
}

if existing != nil {
return fmt.Errorf("identity with email %q already exists", email)
}

if err := client.Omni().State().Create(ctx, user); err != nil {
return err
}

return client.Omni().State().Create(ctx, identity)
}
}

func init() {
createCmd.PersistentFlags().StringVarP(&createCmdFlags.role, "role", "r", "", "Role to use for the user creation")
createCmd.MarkPersistentFlagRequired("role") //nolint:errcheck

userCmd.AddCommand(createCmd)
}
77 changes: 77 additions & 0 deletions client/pkg/omnictl/user/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package user

import (
"context"
"fmt"

"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/spf13/cobra"

"github.com/siderolabs/omni/client/pkg/client"
"github.com/siderolabs/omni/client/pkg/omni/resources"
"github.com/siderolabs/omni/client/pkg/omni/resources/auth"
"github.com/siderolabs/omni/client/pkg/omnictl/internal/access"
)

// deleteCmd represents the user delete command.
var deleteCmd = &cobra.Command{
Use: "delete [email1 email2]",
Short: "Delete users.",
Long: `Delete users with the specified emails.`,
Example: "",
Args: cobra.MinimumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
return access.WithClient(deleteUsers(args...))
},
}

func deleteUsers(emails ...string) func(ctx context.Context, client *client.Client) error {
return func(ctx context.Context, client *client.Client) error {
toDelete := make([]resource.Pointer, 0, len(emails)*2)

for _, email := range emails {
identity := auth.NewIdentity(resources.DefaultNamespace, email)

existing, err := safe.ReaderGetByID[*auth.Identity](ctx, client.Omni().State(), email)
if err != nil {
return err
}

toDelete = append(toDelete, identity.Metadata(), auth.NewUser(resources.DefaultNamespace, existing.TypedSpec().Value.UserId).Metadata())
}

for _, md := range toDelete {
fmt.Printf("tearing down %s %s\n", md.Type(), md.ID())

if _, err := client.Omni().State().Teardown(ctx, md); err != nil {
return err
}
}

for _, md := range toDelete {
_, err := client.Omni().State().WatchFor(ctx, md, state.WithFinalizerEmpty())
if err != nil {
return err
}

err = client.Omni().State().Destroy(ctx, md)
if err != nil {
return err
}

fmt.Printf("destroy %s %s\n", md.Type(), md.ID())
}

return nil
}
}

func init() {
userCmd.AddCommand(deleteCmd)
}
111 changes: 111 additions & 0 deletions client/pkg/omnictl/user/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package user

import (
"context"
"fmt"
"os"
"slices"
"strings"
"text/tabwriter"

"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/spf13/cobra"

"github.com/siderolabs/omni/client/pkg/client"
"github.com/siderolabs/omni/client/pkg/omni/resources/auth"
"github.com/siderolabs/omni/client/pkg/omnictl/internal/access"
)

// listCmd represents the user list command.
var listCmd = &cobra.Command{
Use: "list",
Short: "List all users.",
Long: `List all existing users on the Omni instance.`,
Example: "",
Args: cobra.ExactArgs(0),
RunE: func(*cobra.Command, []string) error {
return access.WithClient(listUsers)
},
}

func listUsers(ctx context.Context, client *client.Client) error {
identities, err := safe.ReaderListAll[*auth.Identity](ctx, client.Omni().State(), state.WithLabelQuery(
resource.LabelExists(auth.LabelIdentityTypeServiceAccount, resource.NotMatches),
))
if err != nil {
return err
}

type user struct {
id string
email string
role string
labels string
}

users, err := safe.ReaderListAll[*auth.User](ctx, client.Omni().State())
if err != nil {
return err
}

userList := safe.ToSlice(identities, func(identity *auth.Identity) user {
res := user{
id: identity.TypedSpec().Value.UserId,
email: identity.Metadata().ID(),
}

u, found := users.Find(func(user *auth.User) bool {
return user.Metadata().ID() == identity.TypedSpec().Value.UserId
})
if !found {
return res
}

res.role = u.TypedSpec().Value.Role

allLabels := identity.Metadata().Labels().Raw()

samlLabels := make([]string, 0, len(allLabels))

for key, value := range allLabels {
if !strings.HasPrefix(key, auth.SAMLLabelPrefix) {
continue
}

samlLabels = append(samlLabels, strings.TrimPrefix(key, auth.SAMLLabelPrefix)+"="+value)
}

slices.Sort(samlLabels)

res.labels = strings.Join(samlLabels, ", ")

return res
})

w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
defer w.Flush() //nolint:errcheck

_, err = fmt.Fprintln(w, "ID\tEMAIL\tROLE\tLABELS")
if err != nil {
return err
}

for _, user := range userList {
_, err = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", user.id, user.email, user.role, user.labels)
if err != nil {
return err
}
}

return nil
}

func init() {
userCmd.AddCommand(listCmd)
}
63 changes: 63 additions & 0 deletions client/pkg/omnictl/user/set_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package user

import (
"context"

"github.com/cosi-project/runtime/pkg/safe"
"github.com/spf13/cobra"

"github.com/siderolabs/omni/client/pkg/client"
"github.com/siderolabs/omni/client/pkg/omni/resources"
"github.com/siderolabs/omni/client/pkg/omni/resources/auth"
"github.com/siderolabs/omni/client/pkg/omnictl/internal/access"
)

var setRoleCmdFlags struct {
role string
}

// setRoleCmd represents the user role set command.
var setRoleCmd = &cobra.Command{
Use: "set-role [email]",
Short: "Update the role of the user.",
Long: `Update the user role.`,
Example: "",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
return access.WithClient(setUserRole(args[0]))
},
}

func setUserRole(email string) func(ctx context.Context, client *client.Client) error {
return func(ctx context.Context, client *client.Client) error {
identity, err := safe.ReaderGetByID[*auth.Identity](ctx, client.Omni().State(), email)
if err != nil {
return err
}

_, err = safe.StateUpdateWithConflicts(ctx, client.Omni().State(),
auth.NewUser(resources.DefaultNamespace, identity.TypedSpec().Value.UserId).Metadata(),
func(user *auth.User) error {
user.TypedSpec().Value.Role = setRoleCmdFlags.role

return nil
},
)
if err != nil {
return err
}

return nil
}
}

func init() {
setRoleCmd.PersistentFlags().StringVarP(&setRoleCmdFlags.role, "role", "r", "", "Role to use")
setRoleCmd.MarkPersistentFlagRequired("role") //nolint:errcheck

userCmd.AddCommand(setRoleCmd)
}
24 changes: 24 additions & 0 deletions client/pkg/omnictl/user/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Package user contains commands related to user operations.
package user

import (
"github.com/spf13/cobra"
)

// userCmd represents the cluster sub-command.
var userCmd = &cobra.Command{
Use: "user",
Aliases: []string{"u"},
Short: "User-related subcommands.",
Long: `Commands to manage users.`,
Example: "",
}

// RootCmd exposes root cluster command.
func RootCmd() *cobra.Command {
return userCmd
}
Loading

0 comments on commit b1ab823

Please sign in to comment.