| // - |
| // |
| // ========================LICENSE_START================================= |
| // O-RAN-SC |
| // %% |
| // Copyright (C) 2022-2023: Nordix Foundation |
| // %% |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // ========================LICENSE_END=================================== |
| package main |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| corev1 "k8s.io/api/core/v1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| kubernetes "k8s.io/client-go/kubernetes" |
| "k8s.io/client-go/rest" |
| "net/http" |
| "net/url" |
| "rapps/utils/pemtojwks" |
| ) |
| |
| const ( |
| namespace = "istio-nonrtric" |
| ) |
| |
| type Jwttoken struct { |
| Access_token string |
| Expires_in int |
| Refresh_expires_in int |
| Refresh_token string |
| Token_type string |
| Not_before_policy int |
| Session_state string |
| Scope string |
| } |
| |
| type RealmRepresentation struct { |
| Id string `json:"id,omitempty"` |
| Realm string `json:"realm,omitempty"` |
| DisplayName string `json:"displayName,omitempty"` |
| Enabled bool `json:"enabled"` |
| } |
| |
| type Client struct { |
| ClientID string `json:"clientId,omitempty"` |
| Enabled bool `json:"enabled,omitempty"` |
| DirectAccessGrantsEnabled bool `json:"directAccessGrantsEnabled,omitempty"` |
| BearerOnly bool `json:"bearerOnly,omitempty"` |
| PublicClient bool `json:"publicClient,omitempty"` |
| ServiceAccountsEnabled bool `json:"serviceAccountsEnabled,omitempty"` |
| ClientAuthenticatorType string `json:"clientAuthenticatorType,omitempty"` |
| DefaultClientScopes []string `json:"defaultClientScopes,omitempty"` |
| Attributes map[string]string `json:"attributes,omitempty"` |
| AuthenticationFlowBindingOverrides map[string]string `json:"authenticationFlowBindingOverrides,omitempty"` |
| } |
| |
| type Role struct { |
| Name string `json:"name,omitempty"` |
| } |
| |
| type User struct { |
| ID string `json:"id,omitempty"` |
| Username string `json:"username,omitempty"` |
| Email string `json:"email,omitempty"` |
| Enabled bool `json:"enabled"` |
| } |
| |
| type ProtocolMapperRepresentation struct { |
| Name string `json:"name,omitempty"` |
| Protocol string `json:"protocol,omitempty"` |
| ProtocolMapper string `json:"protocolMapper,omitempty"` |
| Config map[string]string `json:"config,omitempty"` |
| } |
| |
| type RoleRepresentation struct { |
| ID string `json:"id,omitempty"` |
| Name string `json:"name,omitempty"` |
| Composite bool `json:"composite"` |
| ClientRole bool `json:"clientRole"` |
| } |
| |
| type AuthenticationFlowRepresentation struct { |
| Alias string `json:"alias,omitempty"` |
| Description string `json:"description,omitempty"` |
| ProviderId string `json:"providerId,omitempty"` |
| TopLevel bool `json:"topLevel"` |
| BuiltIn bool `json:"builtIn"` |
| AthenticationExecutions []string `json:"authenticationExecutions,omitempty"` |
| } |
| |
| type Execution struct { |
| Provider string `json:"provider,omitempty"` |
| } |
| |
| type AuthenticatorConfigRepresentation struct { |
| Alias string `json:"alias,omitempty"` |
| Config map[string]string `json:"config,omitempty"` |
| } |
| |
| var keycloakUrl string = "http://keycloak:8080" |
| var token Jwttoken |
| var flowAlias string = "x509 direct grant" |
| |
| func createClient(res http.ResponseWriter, req *http.Request) { |
| body, err := ioutil.ReadAll(req.Body) |
| if err != nil { |
| panic(err.Error()) |
| } |
| keyVal := make(map[string]string) |
| json.Unmarshal(body, &keyVal) |
| realmName := keyVal["realm"] |
| clientName := keyVal["name"] |
| role := keyVal["role"] |
| authType := keyVal["authType"] |
| tlsCrt := keyVal["tlsCrt"] |
| email := keyVal["email"] |
| subjectDN := keyVal["subjectDN"] |
| mappingSource := keyVal["mappingSource"] |
| |
| var msg string |
| msg, err = create(realmName, clientName, role, authType, tlsCrt, email, subjectDN, mappingSource) |
| if err != nil { |
| msg = err.Error() |
| } |
| if authType == "client-secret" { |
| createSecret(msg, clientName, realmName, namespace) |
| } |
| // create response binary data |
| data := []byte(msg) // slice of bytes |
| // write `data` to response |
| res.Write(data) |
| } |
| |
| func removeClient(res http.ResponseWriter, req *http.Request) { |
| query := req.URL.Query() |
| realmName := query.Get("realm") |
| clientName := query.Get("name") |
| authType := query.Get("authType") |
| |
| var msg string = "Removed keycloak " + clientName + " from " + realmName + " realm" |
| remove(realmName, clientName) |
| if authType == "client-secret" { |
| removeSecret(namespace, clientName) |
| } |
| // create response binary data |
| data := []byte(msg) // slice of bytes |
| // write `data` to response |
| res.Write(data) |
| } |
| |
| func main() { |
| createHandler := http.HandlerFunc(createClient) |
| http.Handle("/create", createHandler) |
| removeHandler := http.HandlerFunc(removeClient) |
| http.Handle("/remove", removeHandler) |
| http.ListenAndServe(":9000", nil) |
| } |
| |
| func getAdminToken() { |
| var resp = &http.Response{} |
| var err error |
| username := "admin" |
| password := "admin" |
| clientId := "admin-cli" |
| restUrl := keycloakUrl + "/realms/master/protocol/openid-connect/token" |
| resp, err = http.PostForm(restUrl, |
| url.Values{"username": {username}, "password": {password}, "grant_type": {"password"}, "client_id": {clientId}}) |
| if err != nil { |
| fmt.Println(err) |
| panic("Something wrong with the credentials or url ") |
| } |
| defer resp.Body.Close() |
| body, err := ioutil.ReadAll(resp.Body) |
| json.Unmarshal([]byte(body), &token) |
| } |
| |
| func sendRequest(method, url string, data []byte) (int, string) { |
| fmt.Printf("Sending %s request to %s\n", method, url) |
| req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) |
| req.Header.Set("Content-Type", "application/json") |
| req.Header.Set("Authorization", "Bearer "+token.Access_token) |
| |
| client := &http.Client{} |
| resp, err := client.Do(req) |
| if err != nil { |
| panic(err) |
| } |
| defer resp.Body.Close() |
| body, _ := ioutil.ReadAll(resp.Body) |
| respString := string(body) |
| fmt.Println("response Status:", resp.Status) |
| return resp.StatusCode, respString |
| } |
| |
| func create(realmName, clientName, clientRoleName, authType, tlsCrt, email, subjectDN, mappingSource string) (string, error) { |
| getAdminToken() |
| var userId string = "" |
| var jsonValue []byte = []byte{} |
| restUrl := keycloakUrl + "/realms/" + realmName |
| statusCode, _ := sendRequest("GET", restUrl, nil) |
| |
| if statusCode != 200 { |
| realmRepresentation := RealmRepresentation{ |
| Id: realmName, |
| Realm: realmName, |
| DisplayName: realmName, |
| Enabled: true, |
| } |
| restUrl := keycloakUrl + "/admin/realms" |
| jsonValue, _ := json.Marshal(realmRepresentation) |
| statusCode, _ = sendRequest("POST", restUrl, jsonValue) |
| } |
| |
| var flowId string = "" |
| if authType == "client-x509" { |
| flowId = getFlowId(realmName) |
| if flowId == "" { |
| createx509Flow(realmName, mappingSource) |
| flowId = getFlowId(realmName) |
| } |
| newUser := User{ |
| ID: realmName + "user", |
| Username: realmName + "user", |
| Email: email, |
| Enabled: true, |
| } |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/users" |
| jsonValue, _ = json.Marshal(newUser) |
| statusCode, _ = sendRequest("POST", restUrl, jsonValue) |
| userId = getUserId(realmName, realmName+"user") |
| } |
| |
| newClient := getClient(authType, clientName, flowId, tlsCrt, subjectDN) |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/clients" |
| jsonValue, _ = json.Marshal(newClient) |
| statusCode, _ = sendRequest("POST", restUrl, jsonValue) |
| |
| clientId, clientSecret := getClientInfo(realmName, clientName) |
| |
| newClientRole := Role{ |
| Name: clientRoleName, |
| } |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/clients/" + clientId + "/roles" |
| jsonValue, _ = json.Marshal(newClientRole) |
| statusCode, _ = sendRequest("POST", restUrl, jsonValue) |
| |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/clients/" + clientId + "/roles/" + clientRoleName |
| statusCode, data := sendRequest("GET", restUrl, nil) |
| roles := make(map[string]interface{}) |
| err := json.Unmarshal([]byte(data), &roles) |
| if err != nil { |
| fmt.Println(err) |
| } |
| roleId := fmt.Sprintf("%v", roles["id"]) |
| |
| if authType != "client-x509" { |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/clients/" + clientId + "/service-account-user" |
| statusCode, data = sendRequest("GET", restUrl, nil) |
| serviceAccount := make(map[string]interface{}) |
| err = json.Unmarshal([]byte(data), &serviceAccount) |
| if err != nil { |
| fmt.Println(err) |
| } |
| userId = fmt.Sprintf("%v", serviceAccount["id"]) |
| } |
| |
| roleRepresentation := RoleRepresentation{ |
| ID: roleId, |
| Name: clientRoleName, |
| Composite: false, |
| ClientRole: true, |
| } |
| |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/users/" + userId + "/role-mappings/clients/" + clientId |
| jsonValue, _ = json.Marshal([]RoleRepresentation{roleRepresentation}) |
| statusCode, data = sendRequest("POST", restUrl, jsonValue) |
| |
| clientroleMapper := ProtocolMapperRepresentation{ |
| Name: "Client Role " + clientName + " Mapper", |
| Protocol: "openid-connect", |
| ProtocolMapper: "oidc-usermodel-client-role-mapper", |
| Config: map[string]string{ |
| "access.token.claim": "true", |
| "aggregate.attrs": "", |
| "claim.name": "clientRole", |
| "id.token.claim": "true", |
| "jsonType.label": "String", |
| "multivalued": "true", |
| "usermodel.clientRoleMapping.clientId": clientName, |
| "userinfo.token.claim": "false", |
| }, |
| } |
| |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/clients/" + clientId + "/protocol-mappers/models" |
| jsonValue, _ = json.Marshal(clientroleMapper) |
| statusCode, _ = sendRequest("POST", restUrl, jsonValue) |
| return clientSecret, nil |
| } |
| |
| func getClient(authType, clientName, flowId, tlsCrt, subjectDN string) Client { |
| var newClient Client |
| newClient.ClientID = clientName |
| newClient.Enabled = true |
| newClient.DirectAccessGrantsEnabled = true |
| newClient.BearerOnly = false |
| newClient.PublicClient = false |
| newClient.ServiceAccountsEnabled = true |
| newClient.ClientAuthenticatorType = authType |
| newClient.DefaultClientScopes = []string{"email"} |
| if authType == "client-secret" { |
| newClient.Attributes = map[string]string{ |
| "use.refresh.tokens": "true", |
| "client_credentials.use_refresh_token": "true"} |
| } else if authType == "client-x509" { |
| newClient.Attributes = map[string]string{ |
| "use.refresh.tokens": "true", |
| "client_credentials.use_refresh_token": "true", |
| "x509.subjectdn": ".*" + subjectDN + ".*", |
| "x509.allow.regex.pattern.comparison": "true"} |
| newClient.AuthenticationFlowBindingOverrides = map[string]string{ |
| "direct_grant": flowId} |
| } else { |
| jwksString, publicKey, kid := pemtojwks.CreateJWKS(tlsCrt) |
| newClient.Attributes = map[string]string{ |
| "token.endpoint.auth.signing.alg": "RS256", |
| "jwt.credential.public.key": publicKey, |
| "jwt.credential.kid": kid, |
| "use.jwks.url": "false", |
| "jwks.url": jwksString, |
| "use.refresh.tokens": "true", |
| "client_credentials.use_refresh_token": "true", |
| } |
| } |
| return newClient |
| } |
| |
| func getClientInfo(realmName, clientName string) (string, string) { |
| restUrl := keycloakUrl + "/admin/realms/" + realmName + "/clients?clientId=" + clientName |
| _, data := sendRequest("GET", restUrl, nil) |
| |
| clients := make([]map[string]interface{}, 0) |
| err := json.Unmarshal([]byte(data), &clients) |
| if err != nil { |
| fmt.Println(err) |
| } |
| clientId := fmt.Sprintf("%v", clients[0]["id"]) |
| clientSecret := fmt.Sprintf("%v", clients[0]["secret"]) |
| return clientId, clientSecret |
| } |
| |
| func createx509Flow(realmName, mappingSource string) { |
| var jsonValue []byte = []byte{} |
| authenticationFlowRepresentation := AuthenticationFlowRepresentation{ |
| Alias: flowAlias, |
| Description: "OpenID Connect Resource Owner Grant", |
| ProviderId: "basic-flow", |
| TopLevel: true, |
| BuiltIn: false, |
| AthenticationExecutions: []string{}, |
| } |
| restUrl := keycloakUrl + "/admin/realms/" + realmName + "/authentication/flows" |
| jsonValue, _ = json.Marshal(authenticationFlowRepresentation) |
| sendRequest("POST", restUrl, jsonValue) |
| |
| execution := Execution{ |
| Provider: "direct-grant-auth-x509-username", |
| } |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/authentication/flows/" + flowAlias + "/executions/execution" |
| jsonValue, _ = json.Marshal(execution) |
| sendRequest("POST", restUrl, jsonValue) |
| |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/authentication/flows/" + flowAlias + "/executions" |
| _, data := sendRequest("GET", restUrl, nil) |
| executionInfo := make([]map[string]interface{}, 0) |
| err := json.Unmarshal([]byte(data), &executionInfo) |
| if err != nil { |
| fmt.Println(err) |
| } |
| executionId := fmt.Sprintf("%v", executionInfo[0]["id"]) |
| |
| authenticatorConfigRepresentation := AuthenticatorConfigRepresentation{ |
| Alias: flowAlias + " config", |
| Config: map[string]string{ |
| "x509-cert-auth.canonical-dn-enabled": "false", |
| "x509-cert-auth.serialnumber-hex-enabled": "false", |
| "x509-cert-auth.ocsp-fail-open": "false", |
| "x509-cert-auth.regular-expression": "(.*?)(?:$)", |
| "x509-cert-auth.crl-checking-enabled": "false", |
| "x509-cert-auth.certificate-policy-mode": "All", |
| "x509-cert-auth.timestamp-validation-enabled": "false", |
| "x509-cert-auth.confirmation-page-disallowed": "false", |
| "x509-cert-auth.mapper-selection": "Username or Email", |
| "x509-cert-auth.revalidate-certificate-enabled": "false", |
| "x509-cert-auth.crldp-checking-enabled": "false", |
| "x509-cert-auth.mapping-source-selection": mappingSource, |
| "x509-cert-auth.ocsp-checking-enabled": "false", |
| }, |
| } |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/authentication/executions/" + executionId + "/config" |
| jsonValue, _ = json.Marshal(authenticatorConfigRepresentation) |
| sendRequest("POST", restUrl, jsonValue) |
| } |
| |
| func getFlowId(realmName string) string { |
| var flowId string = "" |
| restUrl := keycloakUrl + "/admin/realms/" + realmName + "/authentication/flows" |
| _, data := sendRequest("GET", restUrl, nil) |
| flows := make([]map[string]interface{}, 0) |
| err := json.Unmarshal([]byte(data), &flows) |
| if err != nil { |
| fmt.Println(err) |
| } |
| |
| for i, _ := range flows { |
| id := fmt.Sprintf("%v", flows[i]["id"]) |
| alias := fmt.Sprintf("%v", flows[i]["alias"]) |
| if alias == flowAlias { |
| flowId = id |
| } |
| } |
| return flowId |
| } |
| |
| func connectToK8s() *kubernetes.Clientset { |
| config, err := rest.InClusterConfig() |
| if err != nil { |
| fmt.Println("failed to create K8s config") |
| } |
| |
| clientset, err := kubernetes.NewForConfig(config) |
| if err != nil { |
| fmt.Println("Failed to create K8s clientset") |
| } |
| |
| return clientset |
| } |
| |
| func createSecret(clientSecret, clientName, realmName, namespace string) { |
| secretName := clientName + "-secret" |
| clientset := connectToK8s() |
| secrets := clientset.CoreV1().Secrets(namespace) |
| secret := &corev1.Secret{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: secretName, |
| Namespace: namespace, |
| Labels: map[string]string{ |
| "app": secretName, |
| }, |
| }, |
| Type: "Opaque", |
| StringData: map[string]string{"client_secret": clientSecret, "client_id": clientName, "realm": realmName}, |
| } |
| |
| _, err := secrets.Create(context.TODO(), secret, metav1.CreateOptions{}) |
| if err != nil { |
| fmt.Println("Failed to create K8s secret.", err) |
| } |
| |
| fmt.Println("Created K8s secret successfully") |
| } |
| |
| func remove(realmName, clientName string) { |
| getAdminToken() |
| clientId, _ := getClientInfo(realmName, clientName) |
| |
| restUrl := keycloakUrl + "/admin/realms/" + realmName + "/clients/" + clientId |
| sendRequest("DELETE", restUrl, nil) |
| |
| var userId string = "" |
| userName := realmName + "user" |
| userId = getUserId(realmName, userName) |
| if userId != "" { |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/users/" + userId |
| sendRequest("DELETE", restUrl, nil) |
| } |
| |
| flowId := getFlowId(realmName) |
| if flowId != "" { |
| restUrl = keycloakUrl + "/admin/realms/" + realmName + "/authentication/flows/" + flowId |
| sendRequest("DELETE", restUrl, nil) |
| } |
| } |
| |
| func getUserId(realmName, userName string) string { |
| var userId string = "" |
| restUrl := keycloakUrl + "/admin/realms/" + realmName + "/users?username=demouser" |
| _, data := sendRequest("GET", restUrl, nil) |
| user := make([]map[string]interface{}, 0) |
| err := json.Unmarshal([]byte(data), &user) |
| if err != nil { |
| fmt.Println(err) |
| } |
| if len(user) > 0 { |
| userId = fmt.Sprintf("%v", user[0]["id"]) |
| } |
| return userId |
| } |
| |
| func removeSecret(namespace, clientName string) { |
| clientset := connectToK8s() |
| secretName := clientName + "-secret" |
| secrets := clientset.CoreV1().Secrets(namespace) |
| err := secrets.Delete(context.TODO(), secretName, metav1.DeleteOptions{}) |
| if err != nil { |
| fmt.Println(err) |
| } else { |
| fmt.Println("Deleted Secret", secretName) |
| } |
| } |