Skip to content

Commit b0c3fdc

Browse files
authored
feat: add command to make user an admin (#10)
1 parent 6fb0d72 commit b0c3fdc

File tree

13 files changed

+551
-7
lines changed

13 files changed

+551
-7
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.24.2
44

55
require (
66
github.com/spf13/cobra v1.9.1
7+
k8s.io/api v0.33.1
78
k8s.io/apimachinery v0.33.1
89
)
910

@@ -40,7 +41,6 @@ require (
4041
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
4142
gopkg.in/inf.v0 v0.9.1 // indirect
4243
gopkg.in/yaml.v3 v3.0.1 // indirect
43-
k8s.io/api v0.33.1 // indirect
4444
k8s.io/klog/v2 v2.130.1 // indirect
4545
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
4646
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect

pkg/cmd/copykeycloakadminpassword.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
ns "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/namespace"
1111
"github.com/spf13/cobra"
1212
"golang.design/x/clipboard"
13-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1413
)
1514

1615
var copyKeycloakAdminPasswordCmd = &cobra.Command{
@@ -42,7 +41,7 @@ func runCopyKeycloakAdminPassword(cmd *cobra.Command, args []string) {
4241
os.Exit(1)
4342
}
4443

45-
secret, err := clients.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
44+
secret, err := k8s.GetSecret(ctx, clients, namespace, secretName)
4645
if err != nil {
4746
fmt.Println(err)
4847
os.Exit(1)

pkg/cmd/makemeadmin.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/git"
9+
"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/github"
10+
"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/k8s"
11+
"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/keycloak"
12+
ns "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/namespace"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
var makeMeAdminCmd = &cobra.Command{
17+
Use: "make-me-admin",
18+
Aliases: []string{"mma"},
19+
Short: "Makes you admin of the current deployment",
20+
Run: makeMeAdmin,
21+
}
22+
23+
func makeMeAdmin(cmd *cobra.Command, args []string) {
24+
ctx := context.Background()
25+
26+
if userEmail == "" {
27+
gitCli, err := git.NewGitCLI("")
28+
if err != nil {
29+
fmt.Println(err)
30+
os.Exit(1)
31+
}
32+
userEmail, err = gitCli.GetUserEmail(ctx)
33+
if err != nil {
34+
fmt.Println(err)
35+
os.Exit(1)
36+
}
37+
}
38+
39+
if namespace == "" {
40+
cli, err := github.NewGitHubCLI("")
41+
if err != nil {
42+
fmt.Println(err)
43+
os.Exit(1)
44+
}
45+
namespace, err = ns.FindCurrentNamespace(ctx, cli)
46+
if err != nil {
47+
fmt.Println(err)
48+
os.Exit(1)
49+
}
50+
}
51+
52+
clients, err := k8s.GetClientset()
53+
if err != nil {
54+
fmt.Println(err)
55+
os.Exit(1)
56+
}
57+
58+
secret, err := k8s.GetSecret(ctx, clients, namespace, secretName)
59+
if err != nil {
60+
fmt.Println(err)
61+
os.Exit(1)
62+
}
63+
64+
username, found := secret.Data[secretKeyUsername]
65+
if !found {
66+
fmt.Printf("The secret did not contain '%s'\n", secretKeyUsername)
67+
os.Exit(1)
68+
}
69+
70+
password, found := secret.Data[secretKey]
71+
if !found {
72+
fmt.Printf("The secret did not contain '%s'\n", secretKey)
73+
os.Exit(1)
74+
}
75+
76+
deploymentURL, err := ns.GetDeploymentURL(namespace)
77+
if err != nil {
78+
fmt.Println(err)
79+
os.Exit(1)
80+
}
81+
82+
kcURL := deploymentURL.JoinPath("./auth")
83+
kcClient, err := keycloak.NewKeycloakClient(kcURL.String())
84+
if err != nil {
85+
fmt.Println(err)
86+
os.Exit(1)
87+
}
88+
89+
err = kcClient.Authenticate(ctx, string(username), string(password))
90+
if err != nil {
91+
fmt.Println(err)
92+
os.Exit(1)
93+
}
94+
95+
userID, err := kcClient.FindUser(ctx, renkuRealm, userEmail)
96+
if err != nil {
97+
fmt.Println(err)
98+
os.Exit(1)
99+
}
100+
101+
isAdmin, err := kcClient.IsRenkuAdmin(ctx, renkuRealm, userID)
102+
if err != nil {
103+
fmt.Println(err)
104+
os.Exit(1)
105+
}
106+
107+
if isAdmin {
108+
fmt.Printf("User '%s' is already a renku admin\n", userEmail)
109+
os.Exit(0)
110+
}
111+
112+
err = kcClient.AddRenkuAdminRoleToUser(ctx, renkuRealm, userID)
113+
if err != nil {
114+
fmt.Println(err)
115+
os.Exit(1)
116+
}
117+
118+
fmt.Println("Done, you are now a Renku admin!")
119+
}
120+
121+
func init() {
122+
makeMeAdminCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "k8s namespace")
123+
makeMeAdminCmd.Flags().StringVar(&secretName, "secret-name", "keycloak-password-secret", "secret name")
124+
makeMeAdminCmd.Flags().StringVar(&secretKey, "secret-key", "KEYCLOAK_ADMIN_PASSWORD", "secret key")
125+
makeMeAdminCmd.Flags().StringVar(&secretKeyUsername, "secret-key-username", "KEYCLOAK_ADMIN", "secret key for the admin username")
126+
makeMeAdminCmd.Flags().StringVar(&renkuRealm, "renku-realm", "Renku", "the Keycloak realm used by renku")
127+
makeMeAdminCmd.Flags().StringVarP(&userEmail, "user-email", "u", "", "your email")
128+
}

pkg/cmd/opendeployment.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cmd
33
import (
44
"context"
55
"fmt"
6-
"net/url"
76
"os"
87
"os/exec"
98
"runtime"
@@ -36,8 +35,7 @@ func openDeployment(cmd *cobra.Command, args []string) {
3635
}
3736
}
3837

39-
// TODO: Can we derive the URL by inspecting ingresses in the k8s namespace?
40-
openURL, err := url.Parse(fmt.Sprintf("https://%s.dev.renku.ch", namespace))
38+
openURL, err := ns.GetDeploymentURL(namespace)
4139
if err != nil {
4240
fmt.Println(err)
4341
os.Exit(1)

pkg/cmd/root.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ import (
1010

1111
var deleteNamespace bool
1212
var namespace string
13-
var secretName string
13+
var renkuRealm string
1414
var secretKey string
15+
var secretKeyUsername string
16+
var secretName string
17+
var userEmail string
1518

1619
var rootCmd = &cobra.Command{
1720
Use: "rdu",
@@ -30,6 +33,7 @@ func runRoot(cmd *cobra.Command, args []string) error {
3033
func init() {
3134
rootCmd.AddCommand(cleanupDeploymentCmd)
3235
rootCmd.AddCommand(copyKeycloakAdminPasswordCmd)
36+
rootCmd.AddCommand(makeMeAdminCmd)
3337
rootCmd.AddCommand(openDeploymentCmd)
3438
rootCmd.AddCommand(versionCmd)
3539
}

pkg/git/cli.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package git
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os/exec"
7+
8+
"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/executils"
9+
)
10+
11+
type GitCLI struct {
12+
git string
13+
}
14+
15+
func NewGitCLI(git string) (*GitCLI, error) {
16+
if git == "" {
17+
git = "git"
18+
}
19+
20+
path, err := exec.LookPath(git)
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
fmt.Printf("Found git: %s", path)
26+
fmt.Println()
27+
return &GitCLI{git: git}, nil
28+
}
29+
30+
func (cli *GitCLI) RunCmd(ctx context.Context, arg ...string) ([]byte, error) {
31+
cmd := exec.CommandContext(ctx, cli.git, arg...)
32+
return executils.FormatOutput(cmd.Output())
33+
}

pkg/git/config.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package git
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
func (cli *GitCLI) GetUserEmail(ctx context.Context) (email string, err error) {
10+
out, err := cli.RunCmd(ctx, "config", "--get", "user.email")
11+
if err != nil {
12+
return "", err
13+
}
14+
15+
email = strings.TrimSpace(string(out))
16+
fmt.Printf("Detected user email: %s\n", email)
17+
return email, nil
18+
}

pkg/k8s/secret.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package k8s
2+
3+
import (
4+
"context"
5+
6+
corev1 "k8s.io/api/core/v1"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/client-go/kubernetes"
9+
)
10+
11+
func GetSecret(ctx context.Context, clients *kubernetes.Clientset, namespace string, secretName string) (secret *corev1.Secret, err error) {
12+
return clients.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
13+
}

pkg/keycloak/admin.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package keycloak
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
)
8+
9+
const renkuAdminRole string = "renku-admin"
10+
11+
func (client *KeycloakClient) FindUser(ctx context.Context, realm string, email string) (userID string, err error) {
12+
getURL := client.GetAdminUsersURL(realm)
13+
14+
query := url.Values{}
15+
query.Set("email", email)
16+
query.Set("exact", "true")
17+
getURL.RawQuery = query.Encode()
18+
19+
var result []getUsersResponse
20+
_, err = client.GetJSON(ctx, getURL.String(), &result)
21+
if err != nil {
22+
return "", err
23+
}
24+
25+
for _, user := range result {
26+
if user.Email == email {
27+
userID = user.ID
28+
fmt.Printf("Found user ID: %s\n", userID)
29+
return userID, nil
30+
}
31+
}
32+
33+
return "", fmt.Errorf("Could not find user '%s' in Keycloak", email)
34+
}
35+
36+
func (client *KeycloakClient) GetAdminUsersURL(realm string) *url.URL {
37+
path := fmt.Sprintf("./admin/realms/%s/users", realm)
38+
return client.BaseURL.JoinPath(path)
39+
}
40+
41+
type getUsersResponse struct {
42+
ID string `json:"id"`
43+
Email string `json:"email"`
44+
}
45+
46+
func (client *KeycloakClient) IsRenkuAdmin(ctx context.Context, realm string, userID string) (isAdmin bool, err error) {
47+
getURL := client.GetAdminRolesURL(realm, userID)
48+
49+
var result []roleMapping
50+
_, err = client.GetJSON(ctx, getURL.String(), &result)
51+
if err != nil {
52+
return false, err
53+
}
54+
55+
for _, role := range result {
56+
if role.Name == renkuAdminRole {
57+
return true, nil
58+
}
59+
}
60+
return false, nil
61+
}
62+
63+
func (client *KeycloakClient) findRenkuAdminRole(ctx context.Context, realm string, userID string) (role roleMapping, err error) {
64+
getURL := client.GetAdminAvailavleRolesURL(realm, userID)
65+
66+
var result []roleMapping
67+
_, err = client.GetJSON(ctx, getURL.String(), &result)
68+
if err != nil {
69+
return roleMapping{}, err
70+
}
71+
72+
for _, roleObj := range result {
73+
if roleObj.Name == renkuAdminRole {
74+
role = roleObj
75+
return role, err
76+
}
77+
}
78+
return roleMapping{}, fmt.Errorf("Could not find role '%s' in Keycloak", renkuAdminRole)
79+
}
80+
81+
func (client *KeycloakClient) AddRenkuAdminRoleToUser(ctx context.Context, realm string, userID string) error {
82+
role, err := client.findRenkuAdminRole(ctx, realm, userID)
83+
84+
postURL := client.GetAdminRolesURL(realm, userID)
85+
86+
body := []roleMapping{
87+
role,
88+
}
89+
90+
_, err = client.PostJSON(ctx, postURL.String(), body, nil)
91+
if err != nil {
92+
return err
93+
}
94+
return nil
95+
}
96+
97+
func (client *KeycloakClient) GetAdminRolesURL(realm string, userID string) *url.URL {
98+
path := fmt.Sprintf("./admin/realms/%s/users/%s/role-mappings/realm", realm, userID)
99+
return client.BaseURL.JoinPath(path)
100+
}
101+
102+
func (client *KeycloakClient) GetAdminAvailavleRolesURL(realm string, userID string) *url.URL {
103+
path := fmt.Sprintf("./admin/realms/%s/users/%s/role-mappings/realm/available", realm, userID)
104+
return client.BaseURL.JoinPath(path)
105+
}
106+
107+
type roleMapping struct {
108+
ID string `json:"id,omitempty"`
109+
ContainerID string `json:"containerId,omitempty"`
110+
Name string `json:"name,omitempty"`
111+
}

0 commit comments

Comments
 (0)