1
+ package htpasswd
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "fmt"
6
+ "regexp"
7
+ "strings"
8
+
9
+ passwordValidator "github.com/openshift-online/ocm-common/pkg/idp/validations"
10
+ )
11
+
12
+ const ClusterAdminUsername = "cluster-admin"
13
+
14
+ var idRE = regexp .MustCompile (`(?i)^[0-9a-z]+([-_][0-9a-z]+)*$` )
15
+
16
+ // UsernameValidator - copied from rosa/cmd/create/idp/htpasswd.go:259
17
+ func UsernameValidator (val interface {}) error {
18
+ if username , ok := val .(string ); ok {
19
+ if strings .ContainsAny (username , "/:%" ) {
20
+ return fmt .Errorf ("invalid username '%s': " +
21
+ "username must not contain /, :, or %%" , username )
22
+ }
23
+ return nil
24
+ }
25
+ return fmt .Errorf ("can only validate strings, got '%v'" , val )
26
+ }
27
+
28
+ // clusterAdminValidator - copied from rosa/cmd/create/idp/htpasswd.go:270
29
+ func clusterAdminValidator (val interface {}) error {
30
+ if username , ok := val .(string ); ok {
31
+ if username == ClusterAdminUsername {
32
+ return fmt .Errorf ("username '%s' is not allowed. It is preserved for cluster admin creation" , username )
33
+ }
34
+ return nil
35
+ }
36
+ return fmt .Errorf ("can only validate strings, got '%v'" , val )
37
+ }
38
+
39
+ // ValidateIdpName - copied from rosa/cmd/create/idp/cmd.go:448
40
+ func ValidateIdpName (idpName interface {}) error {
41
+ name , ok := idpName .(string )
42
+ if ! ok {
43
+ return fmt .Errorf ("Invalid type for identity provider name. Expected a string, got %T" , idpName )
44
+ }
45
+
46
+ if ! idRE .MatchString (name ) {
47
+ return fmt .Errorf ("Invalid identifier '%s' for 'name'" , idpName )
48
+ }
49
+
50
+ if strings .EqualFold (name , "cluster-admin" ) {
51
+ return fmt .Errorf ("The name \" cluster-admin\" is reserved for admin user IDP" )
52
+ }
53
+ return nil
54
+ }
55
+
56
+ // validateHtUsernameAndPassword - copied from rosa/cmd/create/idp/htpasswd.go:318
57
+ func validateHtUsernameAndPassword (username , password string ) error {
58
+ err := UsernameValidator (username )
59
+ if err != nil {
60
+ return err
61
+ }
62
+ err = clusterAdminValidator (username )
63
+ if err != nil {
64
+ return err
65
+ }
66
+ err = passwordValidator .PasswordValidator (password )
67
+ if err != nil {
68
+ return err
69
+ }
70
+ return nil
71
+ }
72
+
73
+ // parseHtpasswordFile - copied from rosa/cmd/create/idp/htpasswd.go:281
74
+ func parseHtpasswordFile (usersList * map [string ]string , fileContent string ) error {
75
+ lines := strings .Split (fileContent , "\n " )
76
+ for _ , line := range lines {
77
+ line = strings .TrimSpace (line )
78
+ if line == "" {
79
+ continue
80
+ }
81
+
82
+ // split "user:password" at colon
83
+ username , password , found := strings .Cut (line , ":" )
84
+ if ! found || username == "" || password == "" {
85
+ return fmt .Errorf ("Malformed line, Expected: validUsername:validPassword, Got: %s" , line )
86
+ }
87
+
88
+ (* usersList )[username ] = password
89
+ }
90
+ return nil
91
+ }
92
+
93
+ // ProcessUserInput - processes MCP tool parameters using ROSA CLI patterns
94
+ func ProcessUserInput (userInput map [string ]interface {}) (map [string ]string , bool , error ) {
95
+ userList := make (map [string ]string )
96
+ isHashedPassword := false
97
+
98
+ // Option 1: users array (comma-separated list like ROSA CLI)
99
+ if users , ok := userInput ["users" ].([]interface {}); ok && len (users ) > 0 {
100
+ for _ , user := range users {
101
+ userStr , ok := user .(string )
102
+ if ! ok {
103
+ return nil , false , fmt .Errorf ("invalid user format, expected string" )
104
+ }
105
+ username , password , found := strings .Cut (userStr , ":" )
106
+ if ! found {
107
+ return nil , false , fmt .Errorf ("users should be provided in format username:password" )
108
+ }
109
+ userList [username ] = password
110
+ }
111
+ return userList , false , nil
112
+ }
113
+
114
+ // Option 2: single username/password (ROSA CLI backward compatibility)
115
+ if username , hasUsername := userInput ["username" ].(string ); hasUsername {
116
+ if password , hasPassword := userInput ["password" ].(string ); hasPassword {
117
+ userList [username ] = password
118
+ return userList , false , nil
119
+ }
120
+ return nil , false , fmt .Errorf ("password required when username is provided" )
121
+ }
122
+
123
+ // Option 3: htpasswd file content
124
+ if fileContent , ok := userInput ["htpasswd_file_content" ].(string ); ok && fileContent != "" {
125
+ // Decode base64 content
126
+ decoded , err := base64 .StdEncoding .DecodeString (fileContent )
127
+ if err != nil {
128
+ return nil , false , fmt .Errorf ("failed to decode htpasswd file content: %w" , err )
129
+ }
130
+
131
+ // Use ROSA CLI parsing function
132
+ if err := parseHtpasswordFile (& userList , string (decoded )); err != nil {
133
+ return nil , false , fmt .Errorf ("failed to parse htpasswd file: %w" , err )
134
+ }
135
+ isHashedPassword = true // htpasswd files contain pre-hashed passwords
136
+ return userList , isHashedPassword , nil
137
+ }
138
+
139
+ return nil , false , fmt .Errorf ("no user input provided: specify 'users', 'username'+'password', or 'htpasswd_file_content'" )
140
+ }
141
+
142
+ // ValidateUserCredentials - validates username and password using ROSA CLI validation
143
+ func ValidateUserCredentials (username , password string ) error {
144
+ return validateHtUsernameAndPassword (username , password )
145
+ }
0 commit comments