• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FIWARE / credentials-config-service / #57

13 Jan 2026 07:06AM UTC coverage: 83.163% (-0.4%) from 83.552%
#57

push

web-flow
MIgrate to liquibase (#22)

* refactor(db): replace flyway with liquibase

Liquibase has better support for different types of databases. It allows having a single migration file for all databases.

* do not copy test resources into prod image

* update README.md

* rename 2.0.3 database migration script

---------

Co-authored-by: Stefan Wiedemann <wistefan@googlemail.com>

0 of 3 new or added lines in 1 file covered. (0.0%)

19 existing lines in 3 files now uncovered.

568 of 683 relevant lines covered (83.16%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.35
/src/main/java/org/fiware/iam/rest/ServiceApiController.java
1
package org.fiware.iam.rest;
2

3
import io.micronaut.core.annotation.NonNull;
4
import io.micronaut.core.annotation.Nullable;
5
import io.micronaut.data.model.Page;
6
import io.micronaut.data.model.Pageable;
7
import io.micronaut.data.model.Sort;
8
import io.micronaut.http.HttpResponse;
9
import io.micronaut.http.annotation.Controller;
10
import io.micronaut.transaction.annotation.Transactional;
11
import lombok.RequiredArgsConstructor;
12
import lombok.extern.slf4j.Slf4j;
13
import org.fiware.iam.ServiceMapper;
14
import org.fiware.iam.ccs.api.ServiceApi;
15
import org.fiware.iam.ccs.model.*;
16
import org.fiware.iam.exception.ConflictException;
17
import org.fiware.iam.repository.ScopeEntry;
18
import org.fiware.iam.repository.ScopeEntryRepository;
19
import org.fiware.iam.repository.Service;
20
import org.fiware.iam.repository.ServiceRepository;
21

22
import java.net.URI;
23
import java.util.HashSet;
24
import java.util.List;
25
import java.util.Map;
26
import java.util.Optional;
27
import java.util.stream.Collectors;
28

29
/**
30
 * Implementation of the service api to configure services and there credentials
31
 */
32
@Slf4j
1✔
33
@Controller("${general.basepath:/}")
34
@RequiredArgsConstructor
35
public class ServiceApiController implements ServiceApi {
36

37
        private final ServiceRepository serviceRepository;
38
    private final ScopeEntryRepository scopeEntryRepository;
39
        private final ServiceMapper serviceMapper;
40

41
        @Override
42
        public HttpResponse<Object> createService(@NonNull ServiceVO serviceVO) {
43
                if (serviceVO.getId() != null && serviceRepository.existsById(serviceVO.getId())) {
1✔
44
                        throw new ConflictException(String.format("The service with id %s already exists.", serviceVO.getId()),
1✔
45
                                        serviceVO.getId());
1✔
46
                }
47
                validateServiceVO(serviceVO);
1✔
48

49
                Service mappedService = serviceMapper.map(serviceVO);
1✔
50

51
                Service savedService = serviceRepository.save(mappedService);
1✔
52

53
                return HttpResponse.created(
1✔
54
                                URI.create(
1✔
55
                                                ServiceApi.PATH_GET_SERVICE.replace(
1✔
56
                                                                "{id}", savedService.getId())));
1✔
57
        }
58

59
    @Transactional
60
    @Override
61
    public HttpResponse<Object> deleteServiceById(@NonNull String id) {
62
        Optional<Service> service = serviceRepository.findById(id);
1✔
63

64
        if (service.isEmpty()) {
1✔
65
            return HttpResponse.notFound();
1✔
66
        }
67

68
        scopeEntryRepository.deleteByService(service.get());
1✔
69
        serviceRepository.deleteById(id);
1✔
70

71
        return HttpResponse.noContent();
1✔
72
    }
73

74
        @Override
75
        public HttpResponse<List<String>> getScopeForService(@NonNull String id, @Nullable String oidcScope) {
76
                Optional<Service> service = serviceRepository.findById(id);
1✔
77
                if (service.isEmpty()) {
1✔
78
                        return HttpResponse.notFound();
1✔
79
                }
80
                String selectedOidcScope =
81
                                oidcScope == null ?
1✔
82
                                                serviceMapper.map(service.get()).getDefaultOidcScope() :
1✔
83
                                                oidcScope;
1✔
84
                ServiceScopesEntryVO serviceScopesEntryVO = serviceMapper.map(service.get())
1✔
85
                                .getOidcScopes()
1✔
86
                                .get(selectedOidcScope);
1✔
87
                return HttpResponse.ok(
1✔
88
                                Optional.ofNullable(
1✔
89
                                                                serviceScopesEntryVO.getCredentials())
1✔
90
                                                .orElse(List.of()).stream().map(CredentialVO::getType).toList());
1✔
91
        }
92

93
        @Override
94
        public HttpResponse<ServiceVO> getService(@NonNull String id) {
95
                Optional<ServiceVO> serviceVO = serviceRepository.findById(id)
1✔
96
                                .map(serviceMapper::map);
1✔
97
                return serviceVO
1✔
98
                                .map(HttpResponse::ok)
1✔
99
                                .orElse(HttpResponse.notFound());
1✔
100
        }
101

102
        @Override
103
        public HttpResponse<ServicesVO> getServices(@Nullable Integer nullablePageSize,
104
                                                                                                @Nullable Integer nullablePage) {
105
                var pageSize = Optional.ofNullable(nullablePageSize).orElse(100);
1✔
106
                var page = Optional.ofNullable(nullablePage).orElse(0);
1✔
107
                if (pageSize < 1) {
1✔
108
                        throw new IllegalArgumentException("PageSize has to be at least 1.");
1✔
109
                }
110
                if (page < 0) {
1✔
111
                        throw new IllegalArgumentException("Offsets below 0 are not supported.");
1✔
112
                }
113

114
                Page<Service> requestedPage = serviceRepository.findAll(
1✔
115
                                Pageable.from(page, pageSize, Sort.of(Sort.Order.asc("id"))));
1✔
116
                return HttpResponse.ok(
1✔
117
                                new ServicesVO()
118
                                                .total((int) requestedPage.getTotalSize())
1✔
119
                                                .pageNumber(page)
1✔
120
                                                .pageSize(requestedPage.getContent().size())
1✔
121
                                                .services(requestedPage.getContent().stream().map(serviceMapper::map).toList()));
1✔
122
        }
123

124
@Transactional
125
    @Override
126
    public HttpResponse<ServiceVO> updateService(@NonNull String id, @NonNull ServiceVO serviceVO) {
127
        if (serviceVO.getId() != null && !id.equals(serviceVO.getId())) {
1✔
UNCOV
128
            throw new IllegalArgumentException("The id of a service cannot be updated.");
×
129
        }
130
        validateServiceVO(serviceVO);
1✔
131

132
        Optional<Service> optionalOriginalService = serviceRepository.findById(id);
1✔
133
        if (optionalOriginalService.isEmpty()) {
1✔
134
            return HttpResponse.notFound();
1✔
135
        }
136

137
        Service originalService = optionalOriginalService.get();
1✔
138

139
        // Delete all existing scope entries for this service to avoid duplicates/orphans
140
        // This is necessary because Micronaut Data JDBC doesn't automatically handle orphan removal in updates
141
        scopeEntryRepository.deleteByService(originalService);
1✔
142

143
        Service toBeUpdated = serviceMapper.map(serviceVO);
1✔
144
        toBeUpdated.setId(id);
1✔
145
        Service updatedService = serviceRepository.update(toBeUpdated);
1✔
146
        
147
        return HttpResponse.ok(serviceMapper.map(updatedService));
1✔
148
    }
149

150
        // validate a service vo, e.g. check forbidden null values
151
        private void validateServiceVO(ServiceVO serviceVO) {
152
                if (serviceVO.getDefaultOidcScope() == null) {
1✔
153
                        throw new IllegalArgumentException("Default OIDC scope cannot be null.");
1✔
154
                }
155
                if (serviceVO.getOidcScopes() == null) {
1✔
156
                        throw new IllegalArgumentException("OIDC scopes cannot be null.");
1✔
157
                }
158

159
                String defaultOidcScope = serviceVO.getDefaultOidcScope();
1✔
160
                ServiceScopesEntryVO serviceScopesEntryVO = serviceVO
1✔
161
                                .getOidcScopes()
1✔
162
                                .get(defaultOidcScope);
1✔
163
                if (serviceScopesEntryVO == null) {
1✔
164
                        throw new IllegalArgumentException("Default OIDC scope must exist in OIDC scopes array.");
×
165
                }
166

167
                Optional<CredentialVO> nullType = Optional.ofNullable(serviceScopesEntryVO
1✔
168
                                                .getCredentials())
1✔
169
                                .orElse(List.of())
1✔
170
                                .stream()
1✔
171
                                .filter(cvo -> cvo.getType() == null)
1✔
172
                                .findFirst();
1✔
173
                if (nullType.isPresent()) {
1✔
174
                        throw new IllegalArgumentException("Type of a credential cannot be null.");
1✔
175
                }
176

177
                serviceVO
1✔
178
                                .getOidcScopes()
1✔
179
                                .values()
1✔
180
                                .forEach(this::validateKeyMappings);
1✔
181
        }
1✔
182

183
        private void validateKeyMappings(ServiceScopesEntryVO scopeEntry) {
184
                if (scopeEntry.getFlatClaims()) {
1✔
185
                        List<String> includedKeys = Optional.ofNullable(scopeEntry.getCredentials())
×
186
                                        .orElse(List.of())
×
187
                                        .stream()
×
188
                                        .filter(cvo -> cvo.getJwtInclusion().getEnabled())
×
189
                                        .flatMap(credentialVO ->
×
190
                                                        Optional.ofNullable(credentialVO.getJwtInclusion()
×
191
                                                                                        .getClaimsToInclude())
×
192
                                                                        .orElse(List.of())
×
193
                                                                        .stream()
×
194
                                                                        .map(claim -> {
×
195
                                                                                if (claim.getNewKey() != null && !claim.getNewKey().isEmpty()) {
×
196
                                                                                        return claim.getNewKey();
×
197
                                                                                }
198
                                                                                return claim.getOriginalKey();
×
199
                                                                        })
200
                                        ).toList();
×
201
                        if (includedKeys.size() != new HashSet(includedKeys).size()) {
×
202
                                throw new IllegalArgumentException("Configuration contains duplicate claim keys.");
×
203
                        }
204
                } else {
×
205
                        Optional.ofNullable(scopeEntry.getCredentials())
1✔
206
                                        .orElse(List.of())
1✔
207
                                        .stream()
1✔
208
                                        .filter(cvo -> cvo.getJwtInclusion().getEnabled())
1✔
209
                                        .forEach(cvo -> {
1✔
210
                                                List<String> claimKeys = Optional.ofNullable(cvo.getJwtInclusion()
1✔
211
                                                                                .getClaimsToInclude())
1✔
212
                                                                .orElse(List.of())
1✔
213
                                                                .stream()
1✔
214
                                                                .map(claim -> {
1✔
215
                                                                        if (claim.getNewKey() != null && !claim.getNewKey().isEmpty()) {
1✔
216
                                                                                return claim.getNewKey();
1✔
217
                                                                        }
218
                                                                        return claim.getOriginalKey();
×
219
                                                                })
220
                                                                .toList();
1✔
221
                                                if (claimKeys.size() != new HashSet(claimKeys).size()) {
1✔
222
                                                        throw new IllegalArgumentException("Configuration contains duplicate claim keys.");
×
223
                                                }
224
                                        });
1✔
225
                }
226
        }
1✔
227
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc