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

feat: implement CLI commands for user management #763

Merged
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
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(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the commands seem to be untested (?)

even adding a simple integration test to create/delete/update a user would ensure the commands are not broken in the future

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea. Added the CLI tests for it.

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
Loading