/
accreditationsmodule.go
206 lines (178 loc) · 7.25 KB
/
accreditationsmodule.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package keycloakb
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/cloudtrust/common-service/configuration"
errorhandler "github.com/cloudtrust/common-service/errors"
"github.com/cloudtrust/common-service/validation"
"github.com/cloudtrust/keycloak-bridge/internal/constants"
kc "github.com/cloudtrust/keycloak-client"
)
var (
dateLayout = constants.SupportedDateLayouts[0]
)
const (
// CredsIDNow identifies the condition for IDNow service
CredsIDNow = configuration.CheckKeyIDNow
// CredsPhysical identifies the condition for physical identification
CredsPhysical = configuration.CheckKeyPhysical
)
// AccreditationsModule interface
type AccreditationsModule interface {
GetUserAndPrepareAccreditations(ctx context.Context, accessToken, realmName, userID, condition string) (kc.UserRepresentation, int, error)
}
// AccredsKeycloakClient is the minimum Keycloak client interface for accreditations
type AccredsKeycloakClient interface {
UpdateUser(accessToken string, realmName, userID string, user kc.UserRepresentation) error
GetUser(accessToken string, realmName, userID string) (kc.UserRepresentation, error)
GetRealm(accessToken string, realmName string) (kc.RealmRepresentation, error)
}
// AdminConfigurationDBModule interface
type AdminConfigurationDBModule interface {
GetAdminConfiguration(context.Context, string) (configuration.RealmAdminConfiguration, error)
}
type accredsModule struct {
keycloakClient AccredsKeycloakClient
confDBModule AdminConfigurationDBModule
logger Logger
}
// AccreditationRepresentation is a representation of accreditations
type AccreditationRepresentation struct {
Type *string `json:"type,omitempty"`
ExpiryDate *string `json:"expiryDate,omitempty"`
Revoked *bool `json:"revoked,omitempty"`
}
// IsUpdated checks if there are changes in provided values.
// These values are provided by pair: first one is the new value (or nil if no update is expected) and the second one is the former value
func IsUpdated(values ...*string) bool {
for i := 0; i < len(values)-1; i += 2 {
var newValue = values[i]
var formerValue = values[i+1]
if newValue != nil && (formerValue == nil || !strings.EqualFold(*newValue, *formerValue)) {
return true
}
}
return false
}
// RevokeAccreditations revokes active accreditations of the given user
func RevokeAccreditations(kcUser *kc.UserRepresentation) {
var kcAccreds = kcUser.GetAttribute(constants.AttrbAccreditations)
if len(kcAccreds) == 0 {
return
}
var newAccreds []string
for _, accred := range kcAccreds {
newAccreds = append(newAccreds, revoke(accred))
}
kcUser.SetAttribute(constants.AttrbAccreditations, newAccreds)
}
func revoke(accredJSON string) string {
var accred AccreditationRepresentation
if err := json.Unmarshal([]byte(accredJSON), &accred); err == nil {
var today = time.Now()
if expiry, err := time.Parse(dateLayout, *accred.ExpiryDate); err == nil && today.Before(expiry) {
var bTrue = true
accred.Revoked = &bTrue
var bytes, _ = json.Marshal(accred)
accredJSON = string(bytes)
}
}
return accredJSON
}
// NewAccreditationsModule creates an accreditations module
func NewAccreditationsModule(keycloakClient AccredsKeycloakClient, confDBModule AdminConfigurationDBModule, logger Logger) AccreditationsModule {
return &accredsModule{
keycloakClient: keycloakClient,
confDBModule: confDBModule,
logger: logger,
}
}
func (am *accredsModule) GetUserAndPrepareAccreditations(ctx context.Context, accessToken, realmName, userID, condition string) (kc.UserRepresentation, int, error) {
var kcUser kc.UserRepresentation
// Gets the realm
var realm, err = am.keycloakClient.GetRealm(accessToken, realmName)
if err != nil {
am.logger.Warn(ctx, "msg", "getKeycloakRealm: can't get realm from KC", "err", err.Error())
return kcUser, 0, errorhandler.CreateInternalServerError("keycloak")
}
// Retrieve admin configuration from configuration DB
var rac configuration.RealmAdminConfiguration
rac, err = am.confDBModule.GetAdminConfiguration(ctx, *realm.ID)
if err != nil {
am.logger.Warn(ctx, "msg", "CreateAccreditations: can't get admin configuration", "err", err.Error())
return kcUser, 0, errorhandler.CreateInternalServerError("keycloak")
}
// Evaluate accreditations to be created
var newAccreds map[string]string
newAccreds, err = am.evaluateAccreditations(ctx, rac.Accreditations, condition)
if err != nil {
am.logger.Warn(ctx, "msg", "Can't evaluate accreditations", "err", err.Error())
return kcUser, 0, err
}
// Get the user from Keycloak
kcUser, err = am.keycloakClient.GetUser(accessToken, realmName, userID)
if err != nil {
am.logger.Warn(ctx, "msg", "CreateAccreditations: can't get Keycloak user", "err", err.Error(), "realm", realmName, "user", userID)
return kcUser, 0, err
}
var added = len(newAccreds)
if added == 0 {
return kcUser, 0, errorhandler.CreateInternalServerError("noConfiguredAccreditations")
}
var mergedAccreds = am.evaluateCurrentAccreditations(kcUser.GetAttribute(constants.AttrbAccreditations), newAccreds)
// Merge all attributes
for _, accValue := range newAccreds {
mergedAccreds = append(mergedAccreds, accValue)
}
kcUser.SetAttribute(constants.AttrbAccreditations, mergedAccreds)
return kcUser, added, nil
}
func (am *accredsModule) evaluateCurrentAccreditations(accredsAttributes []string, newAccreds map[string]string) []string {
var accreds []string
var bTrue = true
for _, accredJSON := range accredsAttributes {
var accreditation AccreditationRepresentation
// If accreditation can be unmarshalled
if err := json.Unmarshal([]byte(accredJSON), &accreditation); err==nil {
// If new accreditation is being created for the same type and it is not yet revoked
if _, ok := newAccreds[*accreditation.Type]; ok && (accreditation.Revoked==nil || !*accreditation.Revoked) {
// If existing accreditation is not yet expired
if expiryDate, err := time.Parse(constants.SupportedDateLayouts[0], *accreditation.ExpiryDate); err!=nil || expiryDate.After(time.Now()) {
accreditation.Revoked = &bTrue
bytes, _ := json.Marshal(accreditation)
accredJSON = string(bytes)
}
}
}
accreds = append(accreds, accredJSON)
}
return accreds
}
func (am *accredsModule) evaluateAccreditations(ctx context.Context, accreds []configuration.RealmAdminAccreditation, condition string) (map[string]string, error) {
var newAccreds = make(map[string]string)
for _, modelAccred := range accreds {
if modelAccred.Condition == nil || *modelAccred.Condition == condition {
var expiry, err = am.convertDurationToDate(ctx, *modelAccred.Validity)
if err != nil {
return nil, err
}
var newAccreditationJSON, _ = json.Marshal(AccreditationRepresentation{
Type: modelAccred.Type,
ExpiryDate: expiry,
})
newAccreds[*modelAccred.Type] = string(newAccreditationJSON)
}
}
return newAccreds, nil
}
func (am *accredsModule) convertDurationToDate(ctx context.Context, validity string) (*string, error) {
var expiryDate, err = validation.AddLargeDurationE(time.Now(), validity)
if err != nil {
am.logger.Warn(ctx, "msg", "convertDurationToDate: can't convert duration", "duration", validity, "err", err.Error())
return nil, errorhandler.CreateInternalServerError("duration-convertion")
}
var res = expiryDate.Format(dateLayout)
return &res, nil
}