Improvement: get realm from config file
[nonrtric/plt/sme.git] / capifcore / internal / keycloak / keycloak.go
1 // -
2 //   ========================LICENSE_START=================================
3 //   O-RAN-SC
4 //   %%
5 //   Copyright (C) 2023: Nordix Foundation
6 //   %%
7 //   Licensed under the Apache License, Version 2.0 (the "License");
8 //   you may not use this file except in compliance with the License.
9 //   You may obtain a copy of the License at
10 //
11 //        http://www.apache.org/licenses/LICENSE-2.0
12 //
13 //   Unless required by applicable law or agreed to in writing, software
14 //   distributed under the License is distributed on an "AS IS" BASIS,
15 //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 //   See the License for the specific language governing permissions and
17 //   limitations under the License.
18 //   ========================LICENSE_END===================================
19 //
20
21 package keycloak
22
23 import (
24         "encoding/json"
25         "errors"
26         "io"
27         "net/http"
28         "net/url"
29
30         log "github.com/sirupsen/logrus"
31         "oransc.org/nonrtric/capifcore/internal/config"
32         "oransc.org/nonrtric/capifcore/internal/restclient"
33 )
34
35 //go:generate mockery --name AccessManagement
36 type AccessManagement interface {
37         // Get JWT token for a client.
38         // Returns JWT token if client exits and credentials are correct otherwise returns error.
39         GetToken(realm string, data map[string][]string) (Jwttoken, error)
40         // Add new client in keycloak
41         AddClient(clientId string, realm string) error
42         // Returns information about client including secret
43         GetClientRepresentation(clientId string, realm string) (*Client, error)
44 }
45
46 type AdminUser struct {
47         User     string
48         Password string
49 }
50
51 type KeycloakManager struct {
52         keycloakServerUrl string
53         admin             AdminUser
54         realms            map[string]string
55         client            restclient.HTTPClient
56 }
57
58 func NewKeycloakManager(cfg *config.Config, c restclient.HTTPClient) *KeycloakManager {
59
60         keycloakUrl := "http://" + cfg.AuthorizationServer.Host + ":" + cfg.AuthorizationServer.Port
61
62         return &KeycloakManager{
63                 keycloakServerUrl: keycloakUrl,
64                 client:            c,
65                 admin: AdminUser{
66                         User:     cfg.AuthorizationServer.AdminUser.User,
67                         Password: cfg.AuthorizationServer.AdminUser.Password,
68                 },
69                 realms: cfg.AuthorizationServer.Realms,
70         }
71 }
72
73 type Jwttoken struct {
74         AccessToken      string `json:"access_token"`
75         IDToken          string `json:"id_token"`
76         ExpiresIn        int    `json:"expires_in"`
77         RefreshExpiresIn int    `json:"refresh_expires_in"`
78         RefreshToken     string `json:"refresh_token"`
79         TokenType        string `json:"token_type"`
80         NotBeforePolicy  int    `json:"not-before-policy"`
81         SessionState     string `json:"session_state"`
82         Scope            string `json:"scope"`
83 }
84
85 func (km *KeycloakManager) GetToken(realm string, data map[string][]string) (Jwttoken, error) {
86         var jwt Jwttoken
87         realmVal, ok := km.realms[realm]
88         if !ok {
89                 log.Errorf("error realm does not exist\n")
90                 return jwt, errors.New("realm does not exist")
91         }
92         getTokenUrl := km.keycloakServerUrl + "/realms/" + realmVal + "/protocol/openid-connect/token"
93         resp, err := http.PostForm(getTokenUrl, data)
94
95         if err != nil {
96                 return jwt, err
97         }
98
99         defer resp.Body.Close()
100         body, err := io.ReadAll(resp.Body)
101
102         if err != nil {
103                 return jwt, err
104         }
105
106         if resp.StatusCode != http.StatusOK {
107                 return jwt, errors.New(string(body))
108         }
109
110         json.Unmarshal([]byte(body), &jwt)
111         return jwt, nil
112 }
113
114 type Client struct {
115         AdminURL                     string  `json:"adminUrl,omitempty"`
116         AuthorizationServicesEnabled *bool   `json:"authorizationServicesEnabled,omitempty"`
117         BearerOnly                   bool    `json:"bearerOnly,omitempty"`
118         ClientID                     string  `json:"clientId,omitempty"`
119         Enabled                      bool    `json:"enabled,omitempty"`
120         ID                           *string `json:"id,omitempty"`
121         PublicClient                 bool    `json:"publicClient,omitempty"`
122         RootURL                      string  `json:"rootUrl,omitempty"`
123         Secret                       *string `json:"secret,omitempty"`
124         ServiceAccountsEnabled       bool    `json:"serviceAccountsEnabled,omitempty"`
125 }
126
127 func (km *KeycloakManager) AddClient(clientId string, realm string) error {
128
129         data := url.Values{"grant_type": {"password"}, "username": {km.admin.User}, "password": {km.admin.Password}, "client_id": {"admin-cli"}}
130         token, err := km.GetToken("master", data)
131         if err != nil {
132                 log.Errorf("error wrong credentials or url %v\n", err)
133                 return err
134         }
135
136         realmVal, ok := km.realms[realm]
137         if !ok {
138                 log.Errorf("error realm does not exist\n")
139                 return errors.New("realm does not exist")
140         }
141
142         createClientUrl := km.keycloakServerUrl + "/admin/realms/" + realmVal + "/clients"
143         newClient := map[string]interface{}{"clientId": clientId, "serviceAccountsEnabled": true}
144
145         body, err := json.Marshal(newClient)
146         if err != nil {
147                 return err
148         }
149
150         var headers = map[string]string{"Content-Type": "application/json", "Authorization": "Bearer " + token.AccessToken}
151         if err := restclient.Post(createClientUrl, body, headers, km.client); err != nil {
152                 log.Errorf("addClient - error with http request: %+v\n", err)
153                 return err
154         }
155
156         log.Debug("Created new client")
157         return nil
158
159 }
160
161 func (km *KeycloakManager) GetClientRepresentation(clientId string, realm string) (*Client, error) {
162
163         data := url.Values{"grant_type": {"password"}, "username": {km.admin.User}, "password": {km.admin.Password}, "client_id": {"admin-cli"}}
164         token, err := km.GetToken("master", data)
165         if err != nil {
166                 log.Errorf("error wrong credentials or url %v\n", err)
167                 return nil, err
168         }
169
170         realmVal, ok := km.realms[realm]
171         if !ok {
172                 log.Errorf("error realm does not exist\n")
173                 return nil, errors.New("realm does not exist")
174         }
175
176         createClientUrl, _ := url.Parse(km.keycloakServerUrl + "/admin/realms/" + realmVal + "/clients")
177         q := createClientUrl.Query()
178         q.Add("clientId", clientId)
179         createClientUrl.RawQuery = q.Encode()
180
181         var headers = map[string]string{"Content-Type": "application/json", "Authorization": "Bearer " + token.AccessToken}
182
183         if resp, err := restclient.Get(createClientUrl.String(), headers, km.client); err == nil {
184                 var client []Client
185
186                 if err = json.Unmarshal(resp, &client); err != nil {
187                         log.Errorf("error unmarshal keycloak client object: %+v\n", err)
188                         return nil, err
189                 }
190
191                 if len(client) > 0 {
192                         return &client[0], nil
193                 }
194                 return nil, nil
195
196         } else {
197                 log.Errorf("error with http request: %+v\n", err)
198                 return nil, err
199         }
200
201 }