blob: 6e90992ea05468652e9956aa2c76b57e598fd91a [file] [log] [blame]
// -
//
// ========================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)
}
}