1
0

rename Credentials->Profile + Context->Scope (#202)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/202
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig
2025-09-12 11:37:55 +02:00
parent d7d77f60f3
commit bae13d5503
53 changed files with 976 additions and 1080 deletions
@@ -1,24 +0,0 @@
package net.hostsharing.hsadminng.hs.accounts;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsCredentialsContextRbacRepository extends Repository<HsCredentialsContextRbacEntity, UUID> {
@Timed("app.login.context.repo.findAll")
List<HsCredentialsContextRbacEntity> findAll();
@Timed("app.login.context.repo.findByUuid")
Optional<HsCredentialsContextRbacEntity> findByUuid(final UUID id);
@Timed("app.login.context.repo.findByTypeAndQualifier")
Optional<HsCredentialsContextRbacEntity> findByTypeAndQualifier(@NotNull String contextType, @NotNull String qualifier);
@Timed("app.login.context.repo.save")
HsCredentialsContextRbacEntity save(final HsCredentialsContextRbacEntity entity);
}
@@ -1,24 +0,0 @@
package net.hostsharing.hsadminng.hs.accounts;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsCredentialsContextRealRepository extends Repository<HsCredentialsContextRealEntity, UUID> {
@Timed("app.login.context.repo.findAll")
List<HsCredentialsContextRealEntity> findAll();
@Timed("app.login.context.repo.findByUuid")
Optional<HsCredentialsContextRealEntity> findByUuid(final UUID id);
@Timed("app.login.context.repo.findByTypeAndQualifier")
Optional<HsCredentialsContextRealEntity> findByTypeAndQualifier(@NotNull String contextType, @NotNull String qualifier);
@Timed("app.login.context.repo.save")
HsCredentialsContextRealEntity save(final HsCredentialsContextRealEntity entity);
}
@@ -1,43 +0,0 @@
package net.hostsharing.hsadminng.hs.accounts;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsCredentialsRepository extends Repository<HsCredentialsEntity, UUID> {
@Timed("app.login.credentials.repo.findByUuid")
Optional<HsCredentialsEntity> findByUuid(final UUID uuid);
@Timed("app.login.credentials.repo.findByPerson")
List<HsCredentialsEntity> findByPerson(final HsOfficePerson<?> personUuid);
@Timed("app.login.credentials.repo.findByCurrentSubject")
@Query(nativeQuery = true, value = """
WITH RECURSIVE
same_person AS (
SELECT own_credentials.person_uuid
FROM hs_accounts.credentials own_credentials
WHERE own_credentials.uuid = rbac.currentSubjectUuid()
),
represented_persons AS (
SELECT relation.anchorUuid person_uuid
FROM hs_office.relation relation
WHERE relation.type = 'REPRESENTATIVE'
AND relation.holderUuid IN (SELECT person_uuid FROM same_person)
)
SELECT DISTINCT credentials.*
FROM hs_accounts.credentials credentials
WHERE credentials.person_uuid IN (SELECT person_uuid FROM same_person)
OR credentials.person_uuid IN (SELECT person_uuid FROM represented_persons)
""")
List<HsCredentialsEntity> findByCurrentSubject();
@Timed("app.login.credentials.repo.save")
HsCredentialsEntity save(final HsCredentialsEntity entity);
}
@@ -1,7 +1,5 @@
package net.hostsharing.hsadminng.hs.accounts;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
@@ -10,15 +8,15 @@ import java.util.stream.Collectors;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.val;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CurrentLoginUserResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.RbacSubjectResource;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.accounts.generated.api.v1.api.CredentialsApi;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsInsertResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatchResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.api.ProfileApi;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfileInsertResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfilePatchResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfileResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.HsOfficePersonResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
@@ -37,13 +35,12 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ValidationException;
import static java.util.Optional.ofNullable;
import static java.util.Optional.of;
@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "bearerAuth")
public class HsCredentialsController implements CredentialsApi {
public class HsProfileController implements ProfileApi {
@Autowired
private Context context;
@@ -58,7 +55,7 @@ public class HsCredentialsController implements CredentialsApi {
private RbacSubjectRepository subjectRepo;
@Autowired
private CredentialContextResourceToEntityMapper contextMapper;
private ScopeResourceToEntityMapper scopeMapper;
@Autowired
private MessageTranslator messageTranslator;
@@ -67,113 +64,113 @@ public class HsCredentialsController implements CredentialsApi {
private HsOfficePersonRealRepository realPersonRepo;
@Autowired
private HsCredentialsRepository credentialsRepo;
private HsProfileRepository profileRepo;
@Autowired
private RbacSubjectRepository rbacSubjectRepo;
@Override
@Transactional(readOnly = true)
@Timed("app.credentials.credentials.getSingleCredentialsByUuid")
public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid(final UUID credentialsUuid) {
@Timed("app.accounts.profile.getSingleProfileByUuid")
public ResponseEntity<ProfileResource> getSingleProfileByUuid(final UUID profileUuid) {
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
val credentialsEntity = credentialsRepo.findByUuid(credentialsUuid);
if (credentialsEntity.isEmpty()) {
val profileEntity = profileRepo.findByUuid(profileUuid);
if (profileEntity.isEmpty()) {
return ResponseEntity.notFound().build();
}
val result = mapper.map(
credentialsEntity.get(), CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
profileEntity.get(), ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(result);
}
@Override
@Transactional(readOnly = true)
@Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid")
public ResponseEntity<List<CredentialsResource>> getListOfCredentials(
@Timed("app.accounts.profile.getListOfProfileByPersonUuid")
public ResponseEntity<List<ProfileResource>> getListOfProfile(
final String assumedRoles,
final UUID personUuid
) {
context.assumeRoles(assumedRoles);
val credentials = personUuid == null
? credentialsRepo.findByCurrentSubject()
val profile = personUuid == null
? profileRepo.findByCurrentSubject()
: findByPersonUuid(personUuid);
val result = mapper.mapList(
credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
profile, ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(result);
}
@Override
@Transactional
@Timed("app.credentials.credentials.postNewCredentials")
public ResponseEntity<CredentialsResource> postNewCredentials(
final CredentialsInsertResource body
@Timed("app.accounts.profile.postNewProfile")
public ResponseEntity<ProfileResource> postNewProfile(
final ProfileInsertResource body
) {
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
// first create and save the subject to get its UUID
val newlySavedSubject = createSubject(body.getNickname());
// afterward, create and save the credentials entity with the subject's UUID
val newCredentialsEntity = mapper.map(
body, HsCredentialsEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
validateOnCreate(newCredentialsEntity);
// afterward, create and save the profile entity with the subject's UUID
val newProfileEntity = mapper.map(
body, HsProfileEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
validateOnCreate(newProfileEntity);
// switch to the new subject to get access to its own subject RBAC object
context.define("activate newly created self-service subject", null, body.getNickname(), null);
newCredentialsEntity.setSubject(em.merge(newlySavedSubject)); // attached to EM by the new subject
em.persist(newCredentialsEntity); // newCredentialsEntity.uuid == newlySavedSubject.uuid => do not use repository!
newProfileEntity.setSubject(em.merge(newlySavedSubject)); // attached to EM by the new subject
em.persist(newProfileEntity); // newProfileEntity.uuid == newlySavedSubject.uuid => do not use repository!
// return the new credentials as a resource
// return the new profile as a resource
val uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/accounts/credentials/{id}")
.buildAndExpand(newCredentialsEntity.getUuid())
.path("/api/hs/accounts/profiles/{id}")
.buildAndExpand(newProfileEntity.getUuid())
.toUri();
val newCredentialsResource = mapper.map(
newCredentialsEntity, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(newCredentialsResource);
val newProfileResource = mapper.map(
newProfileEntity, ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(newProfileResource);
}
@Override
@Transactional
@Timed("app.credentials.credentials.deleteCredentialsByUuid")
public ResponseEntity<Void> deleteCredentialsByUuid(final UUID credentialsUuid) {
@Timed("app.accounts.profile.deleteProfileByUuid")
public ResponseEntity<Void> deleteProfileByUuid(final UUID profileUuid) {
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
val credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid);
credentialsEntity.getLoginContexts().clear();
validateOnDelete(credentialsEntity);
val profileEntity = em.getReference(HsProfileEntity.class, profileUuid);
profileEntity.getScopes().clear();
validateOnDelete(profileEntity);
em.flush();
em.remove(credentialsEntity);
em.remove(credentialsEntity.getSubject());
em.remove(profileEntity);
em.remove(profileEntity.getSubject());
return ResponseEntity.noContent().build();
}
@Override
@Transactional
@Timed("app.credentials.credentials.patchCredentials")
public ResponseEntity<CredentialsResource> patchCredentials(
final UUID credentialsUuid,
final CredentialsPatchResource body
@Timed("app.accounts.profile.patchProfile")
public ResponseEntity<ProfileResource> patchProfile(
final UUID profileUuid,
final ProfilePatchResource body
) {
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
val current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
val current = profileRepo.findByUuid(profileUuid).orElseThrow();
new HsCredentialsEntityPatcher(contextMapper, current).apply(body);
new HsProfileEntityPatcher(scopeMapper, current).apply(body);
validateOnUpdate(current);
val saved = credentialsRepo.save(current);
val saved = profileRepo.save(current);
val mapped = mapper.map(
saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
saved, ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}
@Override
@Transactional
@Timed("app.credentials.credentials.getCurrentLoginUser")
@Timed("app.accounts.profile.getCurrentLoginUser")
public ResponseEntity<CurrentLoginUserResource> getCurrentLoginUser() {
// define a context without assumed roles, otherwise we cannot access the subject anymore
@@ -182,7 +179,7 @@ public class HsCredentialsController implements CredentialsApi {
// fetch the data
val currentSubjectUuid = context.fetchCurrentSubjectUuid();
val currentSubject = rbacSubjectRepo.findByUuid(currentSubjectUuid);
val person = credentialsRepo.findByUuid(currentSubjectUuid).orElseThrow().getPerson();
val person = profileRepo.findByUuid(currentSubjectUuid).orElseThrow().getPerson();
final boolean isGlobalAdmin = context.isGlobalAdmin();
@@ -191,46 +188,30 @@ public class HsCredentialsController implements CredentialsApi {
return ResponseEntity.ok(result);
}
@Override
@Transactional
@Timed("app.credentials.credentials.credentialsUsed")
public ResponseEntity<CredentialsResource> credentialsUsed(final UUID credentialsUuid) {
context.define();
val current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
current.setOnboardingToken(null);
current.setLastUsed(LocalDateTime.now());
val saved = credentialsRepo.save(current);
val mapped = mapper.map(saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
private void validateOnCreate(final HsProfileEntity newProfileEntity) {
validateReferencedPersonToBeRepresentedByLoginUserPerson(newProfileEntity);
validateNormalUsersOnlyAccessPublicScopes(newProfileEntity);
validateNaturalPersonRequirementOfScopes(newProfileEntity);
}
private void validateOnCreate(final HsCredentialsEntity newCredentialsEntity) {
validateReferencedPersonToBeRepresentedByLoginUserPerson(newCredentialsEntity);
validateNormalUsersOnlyAccessPublicContexts(newCredentialsEntity);
validateNaturalPersonRequirementOfContexts(newCredentialsEntity);
private void validateOnUpdate(final HsProfileEntity current) {
validateNormalUsersOnlyAccessPublicScopes(current);
validateNaturalPersonRequirementOfScopes(current);
validateOwnHsadminProfileMustNotBeRemoved(current);
}
private void validateOnUpdate(final HsCredentialsEntity current) {
validateNormalUsersOnlyAccessPublicContexts(current);
validateNaturalPersonRequirementOfContexts(current);
validateOwnHsadminCredentialsMustNotBeRemoved(current);
private void validateOnDelete(final HsProfileEntity profileEntity) {
validateOwnHsadminProfileMustNotBeRemoved(profileEntity);
}
private void validateOnDelete(final HsCredentialsEntity credentialsEntity) {
validateOwnHsadminCredentialsMustNotBeRemoved(credentialsEntity);
}
private void validateReferencedPersonToBeRepresentedByLoginUserPerson(final HsCredentialsEntity newCredentialsEntity) {
private void validateReferencedPersonToBeRepresentedByLoginUserPerson(final HsProfileEntity newProfileEntity) {
if (context.isGlobalAdmin()) {
return;
}
val referredPersonUuid = newCredentialsEntity.getPerson().getUuid();
val referredPersonUuid = newProfileEntity.getPerson().getUuid();
val currentSubjectUuid = context.fetchCurrentSubjectUuid();
val loginPersonUuid = credentialsRepo.findByUuid(currentSubjectUuid)
.map(HsCredentialsEntity::getPerson)
val loginPersonUuid = profileRepo.findByUuid(currentSubjectUuid)
.map(HsProfileEntity::getPerson)
.map(HsOfficePerson::getUuid)
.orElseThrow();
val representedPersonUuids = realPersonRepo.findPersonsRepresentedByPersonWithUuid(loginPersonUuid)
@@ -238,58 +219,58 @@ public class HsCredentialsController implements CredentialsApi {
if ( !representedPersonUuids.contains(referredPersonUuid)) {
throw new ValidationException(
messageTranslator.translate(
"credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person",
"profile.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person",
loginPersonUuid));
}
}
private void validateNormalUsersOnlyAccessPublicContexts(final HsCredentialsEntity newCredentialsEntity) {
val forbiddenContexts = newCredentialsEntity.getLoginContexts().stream()
private void validateNormalUsersOnlyAccessPublicScopes(final HsProfileEntity newProfileEntity) {
val forbiddenScopes = newProfileEntity.getScopes().stream()
.filter(c -> !c.isPublicAccess() && !context.isGlobalAdmin() )
.toList();
if (!forbiddenContexts.isEmpty()) {
if (!forbiddenScopes.isEmpty()) {
throw new ValidationException(
messageTranslator.translate(
"credentials.access-denied-for-contexts-{0}",
toDisplay(forbiddenContexts)
"profile.access-denied-for-scopes-{0}",
toDisplay(forbiddenScopes)
));
}
}
private void validateNaturalPersonRequirementOfContexts(final HsCredentialsEntity newCredentialsEntity) {
if (newCredentialsEntity.getPerson().getPersonType().equals(HsOfficePersonType.NATURAL_PERSON)) {
private void validateNaturalPersonRequirementOfScopes(final HsProfileEntity newProfileEntity) {
if (newProfileEntity.getPerson().getPersonType().equals(HsOfficePersonType.NATURAL_PERSON)) {
return;
}
val contextsWhichRequireNaturalPerson = newCredentialsEntity.getLoginContexts().stream()
.filter(HsCredentialsContext::isOnlyForNaturalPersons)
val scopesWhichRequireNaturalPerson = newProfileEntity.getScopes().stream()
.filter(HsProfileScope::isOnlyForNaturalPersons)
.toList();
if (!contextsWhichRequireNaturalPerson.isEmpty()) {
if (!scopesWhichRequireNaturalPerson.isEmpty()) {
throw new ValidationException(
messageTranslator.translate(
"credentials.context-requires-natural-person-{0}",
toDisplay(contextsWhichRequireNaturalPerson)
"profile.scope-requires-natural-person-{0}",
toDisplay(scopesWhichRequireNaturalPerson)
));
}
}
private void validateOwnHsadminCredentialsMustNotBeRemoved(final HsCredentialsEntity newCredentialsEntity) {
if (!newCredentialsEntity.getSubject().getUuid().equals(context.fetchCurrentSubjectUuid())) {
private void validateOwnHsadminProfileMustNotBeRemoved(final HsProfileEntity newProfileEntity) {
if (!newProfileEntity.getSubject().getUuid().equals(context.fetchCurrentSubjectUuid())) {
return;
}
val hsadminCredentialsContext = newCredentialsEntity.getLoginContexts().stream()
.filter(HsCredentialsContext::isHsadminContext)
val hsadminProfileScope = newProfileEntity.getScopes().stream()
.filter(HsProfileScope::isHsadminScope)
.toList();
if (hsadminCredentialsContext.isEmpty()) {
if (hsadminProfileScope.isEmpty()) {
throw new ValidationException(
messageTranslator.translate(
"credentials.own-hsadmin-credentials-must-not-be-removed"
"profile.own-hsadmin-profile-must-not-be-removed"
));
}
}
private static String toDisplay(final List<HsCredentialsContextRealEntity> contextsWhichRequireNaturalPerson) {
return contextsWhichRequireNaturalPerson.stream()
.map(HsCredentialsContext::toShortString)
private static String toDisplay(final List<HsProfileScopeRealEntity> scopesWhichRequireNaturalPerson) {
return scopesWhichRequireNaturalPerson.stream()
.map(HsProfileScope::toShortString)
.sorted()
.map(s -> "'" + s + "'")
.collect(Collectors.joining(", "));
@@ -301,14 +282,14 @@ public class HsCredentialsController implements CredentialsApi {
return newRbacSubject;
}
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
private List<HsProfileEntity> findByPersonUuid(final UUID personUuid) {
val person = realPersonRepo.findByUuid(personUuid).orElseThrow(
() -> new EntityNotFoundException(
messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", personUuid)
)
);
return credentialsRepo.findByPerson(person);
return profileRepo.findByPerson(person);
}
@@ -323,9 +304,7 @@ public class HsCredentialsController implements CredentialsApi {
return result;
}
final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
ofNullable(entity.getLastUsed()).ifPresent(
dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC)));
final BiConsumer<HsProfileEntity, ProfileResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
of(entity.getSubject()).ifPresent(
subject -> resource.setNickname(subject.getName())
);
@@ -335,25 +314,25 @@ public class HsCredentialsController implements CredentialsApi {
)
);
resource.setContexts(mapToValidContextResources(entity));
resource.setScopes(mapToValidScopeResources(entity));
};
private List<ContextResource> mapToValidContextResources(final HsCredentialsEntity entity) {
var allContexts = mapper.mapList(entity.getLoginContexts().stream().toList(), ContextResource.class);
return allContexts.stream()
.filter(context -> !context.getOnlyForNaturalPersons() ||
private List<ScopeResource> mapToValidScopeResources(final HsProfileEntity entity) {
var allScopes = mapper.mapList(entity.getScopes().stream().toList(), ScopeResource.class);
return allScopes.stream()
.filter(scope -> !scope.getOnlyForNaturalPersons() ||
entity.getPerson().getPersonType() == HsOfficePersonType.NATURAL_PERSON)
.toList();
}
final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
final BiConsumer<ProfileInsertResource, HsProfileEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
val person = realPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow(
() -> new EntityNotFoundException(
messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", resource.getPersonUuid())
)
);
entity.setLoginContexts(contextMapper.mapCredentialsToContextEntities(resource.getContexts()));
entity.setScopes(scopeMapper.mapProfileToScopeEntities(resource.getScopes()));
entity.setPerson(person);
};
@@ -9,7 +9,6 @@ import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
// import net.hostsharing.hsadminng.rbac.RbacSubjectEntity; // Assuming RbacSubjectEntity exists for the FK relationship
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -20,20 +19,20 @@ import static jakarta.persistence.CascadeType.REFRESH;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity
@Table(schema = "hs_accounts", name = "credentials")
@Table(schema = "hs_accounts", name = "profile")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Stringifyable {
public class HsProfileEntity implements BaseEntity<HsProfileEntity>, Stringifyable {
protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "credentials")
.withProp(HsCredentialsEntity::isActive)
.withProp(HsCredentialsEntity::getEmailAddress)
.withProp(HsCredentialsEntity::getTotpSecrets)
.withProp(HsCredentialsEntity::getPhonePassword)
.withProp(HsCredentialsEntity::getSmsNumber)
protected static Stringify<HsProfileEntity> stringify = stringify(HsProfileEntity.class, "profile")
.withProp(HsProfileEntity::isActive)
.withProp(HsProfileEntity::getEmailAddress)
.withProp(HsProfileEntity::getTotpSecrets)
.withProp(HsProfileEntity::getPhonePassword)
.withProp(HsProfileEntity::getSmsNumber)
.quotedValues(false);
@Id
@@ -46,14 +45,11 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
@ManyToOne(optional = false, fetch = FetchType.EAGER)
@JoinColumn(name = "person_uuid", nullable = false, updatable = false, referencedColumnName = "uuid")
private HsOfficePersonRealEntity person; // TODO.impl: add RBAC-Support to CredentialsEntity, see Story #
private HsOfficePersonRealEntity person; // TODO.impl: add RBAC-Support to ProfileEntity, see Story #
@Version
private int version;
@Column
private LocalDateTime lastUsed;
@Column
private boolean active;
@@ -63,9 +59,6 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
@Column
private Integer globalGid;
@Column
private String onboardingToken;
@Column
private List<String> totpSecrets;
@@ -80,17 +73,17 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
@OneToMany(fetch = FetchType.EAGER, cascade = { MERGE, REFRESH })
@JoinTable(
name = "context_mapping", schema = "hs_accounts",
joinColumns = @JoinColumn(name = "credentials_uuid", referencedColumnName = "uuid"),
inverseJoinColumns = @JoinColumn(name = "context_uuid", referencedColumnName = "uuid")
name = "scope_mapping", schema = "hs_accounts",
joinColumns = @JoinColumn(name = "profile_uuid", referencedColumnName = "uuid"),
inverseJoinColumns = @JoinColumn(name = "scope_uuid", referencedColumnName = "uuid")
)
private Set<HsCredentialsContextRealEntity> loginContexts;
private Set<HsProfileScopeRealEntity> scopes;
public Set<HsCredentialsContextRealEntity> getLoginContexts() {
if ( loginContexts == null ) {
loginContexts = new HashSet<>();
public Set<HsProfileScopeRealEntity> getScopes() {
if ( scopes == null ) {
scopes = new HashSet<>();
}
return loginContexts;
return scopes;
}
public void setSubject(final RbacSubjectEntity subject) {
@@ -1,23 +1,23 @@
package net.hostsharing.hsadminng.hs.accounts;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatchResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfilePatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import java.util.Optional;
public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatchResource> {
public class HsProfileEntityPatcher implements EntityPatcher<ProfilePatchResource> {
private CredentialContextResourceToEntityMapper contextMapper;
private final HsCredentialsEntity entity;
private ScopeResourceToEntityMapper scopeMapper;
private final HsProfileEntity entity;
public HsCredentialsEntityPatcher(final CredentialContextResourceToEntityMapper contextMapper, final HsCredentialsEntity entity) {
this.contextMapper = contextMapper;
public HsProfileEntityPatcher(final ScopeResourceToEntityMapper scopeMapper, final HsProfileEntity entity) {
this.scopeMapper = scopeMapper;
this.entity = entity;
}
@Override
public void apply(final CredentialsPatchResource resource) {
public void apply(final ProfilePatchResource resource) {
if ( resource.getActive() != null ) {
entity.setActive(resource.getActive());
}
@@ -29,8 +29,8 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatc
.ifPresent(entity::setSmsNumber);
OptionalFromJson.of(resource.getPhonePassword())
.ifPresent(entity::setPhonePassword);
if (resource.getContexts() != null) {
contextMapper.syncCredentialsContextEntities(resource.getContexts(), entity.getLoginContexts());
if (resource.getScopes() != null) {
scopeMapper.syncProfileScopeEntities(resource.getScopes(), entity.getScopes());
}
}
@@ -0,0 +1,43 @@
package net.hostsharing.hsadminng.hs.accounts;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsProfileRepository extends Repository<HsProfileEntity, UUID> {
@Timed("app.login.profile.repo.findByUuid")
Optional<HsProfileEntity> findByUuid(final UUID uuid);
@Timed("app.login.profile.repo.findByPerson")
List<HsProfileEntity> findByPerson(final HsOfficePerson<?> personUuid);
@Timed("app.login.profile.repo.findByCurrentSubject")
@Query(nativeQuery = true, value = """
WITH RECURSIVE
same_person AS (
SELECT own_profile.person_uuid
FROM hs_accounts.profile own_profile
WHERE own_profile.uuid = rbac.currentSubjectUuid()
),
represented_persons AS (
SELECT relation.anchorUuid person_uuid
FROM hs_office.relation relation
WHERE relation.type = 'REPRESENTATIVE'
AND relation.holderUuid IN (SELECT person_uuid FROM same_person)
)
SELECT DISTINCT profile.*
FROM hs_accounts.profile profile
WHERE profile.person_uuid IN (SELECT person_uuid FROM same_person)
OR profile.person_uuid IN (SELECT person_uuid FROM represented_persons)
""")
List<HsProfileEntity> findByCurrentSubject();
@Timed("app.login.profile.repo.save")
HsProfileEntity save(final HsProfileEntity entity);
}
@@ -27,14 +27,16 @@ import static net.hostsharing.hsadminng.repr.Symbol.symbol;
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder(builderMethodName = "baseBuilder", toBuilder = true)
@MappedSuperclass
public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<HsCredentialsContext> {
public abstract class HsProfileScope implements Stringifyable, BaseEntity<HsProfileScope> {
private static Stringify<HsCredentialsContext> stringify = stringify(HsCredentialsContext.class, "loginContext")
.withProp(HsCredentialsContext::getType)
.withProp(HsCredentialsContext::getQualifier)
.withProp(HsCredentialsContext::isOnlyForNaturalPersons,
private static Stringify<HsProfileScope> stringify = stringify(HsProfileScope.class, "scope")
.withProp(HsProfileScope::getType)
.withProp(HsProfileScope::getQualifier)
.withProp(
HsProfileScope::isOnlyForNaturalPersons,
value -> value ? symbol("NP-ONLY") : null)
.withProp(HsCredentialsContext::isPublicAccess,
.withProp(
HsProfileScope::isPublicAccess,
value -> value ? symbol("PUBLIC") : symbol("INTERNAL"))
.quotedValues(false)
.withSeparator(":");
@@ -61,7 +63,7 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
@Column(name = "public_access")
private boolean publicAccess;
public boolean isHsadminContext() {
public boolean isHsadminScope() {
return "HSADMIN".equals(type);
}
@@ -6,8 +6,8 @@ import io.micrometer.core.annotation.Timed;
import lombok.val;
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.accounts.generated.api.v1.api.ContextsApi;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource;
import net.hostsharing.hsadminng.accounts.generated.api.v1.api.ScopesApi;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@NoSecurityRequirement
public class HsCredentialsContextsController implements ContextsApi {
public class HsProfileScopeController implements ScopesApi {
@Autowired
private Context context;
@@ -26,20 +26,20 @@ public class HsCredentialsContextsController implements ContextsApi {
private StrictMapper mapper;
@Autowired
private HsCredentialsContextRbacRepository contextRepo;
private HsProfileScopeRbacRepository scopeRepo;
@Override
@Transactional(readOnly = true)
@Timed("app.credentials.contexts.getListOfLoginContexts")
public ResponseEntity<List<ContextResource>> getListOfContexts(final String assumedRoles) {
@Timed("app.accounts.scopes.getListOfScopes")
public ResponseEntity<List<ScopeResource>> getListOfScopes(final String assumedRoles) {
if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
context.assumeRoles(assumedRoles);
}
val isGlobalAdmin = context.isGlobalAdmin();
final var loginContexts = contextRepo.findAll().stream().filter(
context -> context.isPublicAccess() || isGlobalAdmin
final var scopes = scopeRepo.findAll().stream().filter(
scope -> scope.isPublicAccess() || isGlobalAdmin
).toList();
final var result = mapper.mapList(loginContexts, ContextResource.class);
final var result = mapper.mapList(scopes, ScopeResource.class);
return ResponseEntity.ok(result);
}
}
@@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.WITHOUT_IMPLICIT
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity
@Table(schema = "hs_accounts", name = "context") // TODO_impl: RBAC rules for _rv do not yet work properly
@Table(schema = "hs_accounts", name = "scope") // TODO_impl: RBAC rules for _rv do not yet work properly
@SuperBuilder(toBuilder = true)
@Getter
@Setter
@@ -32,11 +32,11 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@AttributeOverrides({
@AttributeOverride(name = "uuid", column = @Column(name = "uuid"))
})
public class HsCredentialsContextRbacEntity extends HsCredentialsContext {
public class HsProfileScopeRbacEntity extends HsProfileScope {
// TODO_impl: RBAC rules for _rv do not yet work properly (remove the X)
public static RbacSpec rbacX() {
return rbacViewFor("credentialsContext", HsCredentialsContextRbacEntity.class)
return rbacViewFor("profileScope", HsProfileScopeRbacEntity.class)
.withIdentityView(SQL.projection("type || ':' || qualifier"))
.withRestrictedViewOrderBy(SQL.expression("type || ':' || qualifier"))
.withoutUpdatableColumns()
@@ -50,6 +50,6 @@ public class HsCredentialsContextRbacEntity extends HsCredentialsContext {
// TODO_impl: RBAC rules for _rv do not yet work properly (remove the X)
public static void mainX(String[] args) throws IOException {
rbacX().generateWithBaseFileName("9-hs-global/950-accounts/9513-hs-credentials-rbac");
rbacX().generateWithBaseFileName("9-hs-global/950-accounts/9513-hs-profile-rbac");
}
}
@@ -0,0 +1,24 @@
package net.hostsharing.hsadminng.hs.accounts;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsProfileScopeRbacRepository extends Repository<HsProfileScopeRbacEntity, UUID> {
@Timed("app.accounts.scope.repo.findAll.rbac")
List<HsProfileScopeRbacEntity> findAll();
@Timed("app.accounts.scope.repo.findByUuid.rbac")
Optional<HsProfileScopeRbacEntity> findByUuid(final UUID id);
@Timed("app.accounts.scope.repo.findByTypeAndQualifier.rbac")
Optional<HsProfileScopeRbacEntity> findByTypeAndQualifier(@NotNull String contextType, @NotNull String qualifier);
@Timed("app.accounts.scope.repo.save.rbac")
HsProfileScopeRbacEntity save(final HsProfileScopeRbacEntity entity);
}
@@ -11,7 +11,7 @@ import lombok.Setter;
import lombok.experimental.SuperBuilder;
@Entity
@Table(schema = "hs_accounts", name = "context")
@Table(schema = "hs_accounts", name = "scope")
@SuperBuilder(toBuilder = true)
@Getter
@Setter
@@ -19,5 +19,5 @@ import lombok.experimental.SuperBuilder;
@AttributeOverrides({
@AttributeOverride(name = "uuid", column = @Column(name = "uuid"))
})
public class HsCredentialsContextRealEntity extends HsCredentialsContext {
public class HsProfileScopeRealEntity extends HsProfileScope {
}
@@ -0,0 +1,24 @@
package net.hostsharing.hsadminng.hs.accounts;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsProfileScopeRealRepository extends Repository<HsProfileScopeRealEntity, UUID> {
@Timed("app.account.scope.repo.findAll.real")
List<HsProfileScopeRealEntity> findAll();
@Timed("app.account.scope.repo.findByUuid.real")
Optional<HsProfileScopeRealEntity> findByUuid(final UUID id);
@Timed("app.account.scope.repo.findByTypeAndQualifier.real")
Optional<HsProfileScopeRealEntity> findByTypeAndQualifier(@NotNull String type, @NotNull String qualifier);
@Timed("app.account.scope.repo.save.real")
HsProfileScopeRealEntity save(final HsProfileScopeRealEntity entity);
}
@@ -3,8 +3,8 @@ package net.hostsharing.hsadminng.hs.accounts;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -14,56 +14,56 @@ import java.util.Set;
import java.util.stream.Collectors;
@Service
public class CredentialContextResourceToEntityMapper {
public class ScopeResourceToEntityMapper {
private final EntityManager em;
private final MessageTranslator messageTranslator;
@Autowired
public CredentialContextResourceToEntityMapper(EntityManager em, MessageTranslator messageTranslator) {
public ScopeResourceToEntityMapper(final EntityManager em, final MessageTranslator messageTranslator) {
this.em = em;
this.messageTranslator = messageTranslator;
}
public Set<HsCredentialsContextRealEntity> mapCredentialsToContextEntities(
List<ContextResource> resources
public Set<HsProfileScopeRealEntity> mapProfileToScopeEntities(
final List<ScopeResource> resources
) {
final var entities = new HashSet<HsCredentialsContextRealEntity>();
syncCredentialsContextEntities(resources, entities);
final var entities = new HashSet<HsProfileScopeRealEntity>();
syncProfileScopeEntities(resources, entities);
return entities;
}
public void syncCredentialsContextEntities(
List<ContextResource> resources,
Set<HsCredentialsContextRealEntity> entities
public void syncProfileScopeEntities(
final List<ScopeResource> resources,
final Set<HsProfileScopeRealEntity> entities
) {
final var resourceUuids = resources.stream()
.map(ContextResource::getUuid)
.map(ScopeResource::getUuid)
.collect(Collectors.toSet());
final var entityUuids = entities.stream()
.map(HsCredentialsContextRealEntity::getUuid)
.map(HsProfileScopeRealEntity::getUuid)
.collect(Collectors.toSet());
entities.removeIf(e -> !resourceUuids.contains(e.getUuid()));
for (final var resource : resources) {
if (!entityUuids.contains(resource.getUuid())) {
final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid());
if (existingContextEntity == null) {
final var existingScopeEntity = em.find(HsProfileScopeRealEntity.class, resource.getUuid());
if (existingScopeEntity == null) {
throw new EntityNotFoundException(
messageTranslator.translate(
"general.{0}-{1}-not-found-or-not-accessible",
"credentials uuid", resource.getUuid()));
"profile uuid", resource.getUuid()));
}
if ((resource.getType() != null && !existingContextEntity.getType().equals(resource.getType())) ||
(resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) {
if ((resource.getType() != null && !existingScopeEntity.getType().equals(resource.getType())) ||
(resource.getQualifier() != null && !existingScopeEntity.getQualifier().equals(resource.getQualifier()))) {
throw new EntityNotFoundException(
messageTranslator.translate(
"credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}",
existingContextEntity, resource));
"profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}",
existingScopeEntity, resource));
}
entities.add(existingContextEntity);
entities.add(existingScopeEntity);
}
}
}
@@ -58,7 +58,7 @@ public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T
private BiFunction<EntityManager, PropertiesProvider, T> computedBy;
@Accessors(makeFinal = true, chain = true, fluent = false)
private ComputeMode computed; // name 'computed' instead 'computeMode' for better readability in property description
private ComputeMode computed; // name 'computed' instead of 'computeMode' for better readability in property description
@Accessors(makeFinal = true, chain = true, fluent = false)
private boolean readOnly;
@@ -13,5 +13,5 @@ map:
- type: string:uuid => java.util.UUID
paths:
/api/hs/accounts/credentials/{credentialsUuid}:
/api/hs/accounts/profiles/{profileUuid}:
null: org.openapitools.jackson.nullable.JsonNullable
@@ -13,20 +13,17 @@ paths:
/api/hs/accounts/current:
$ref: "current.yaml"
# Contexts
# Scopes
/api/hs/accounts/contexts:
$ref: "contexts.yaml"
/api/hs/accounts/scopes:
$ref: "scopes.yaml"
# Credentials
# Profile
/api/hs/accounts/credentials/{credentialsUuid}/used:
$ref: "credentials-with-uuid-used.yaml"
/api/hs/accounts/profiles/{profileUuid}:
$ref: "profile-with-uuid.yaml"
/api/hs/accounts/credentials/{credentialsUuid}:
$ref: "credentials-with-uuid.yaml"
/api/hs/accounts/credentials:
$ref: "credentials.yaml"
/api/hs/accounts/profiles:
$ref: "profiles.yaml"
@@ -1,23 +0,0 @@
post:
tags:
- credentials
description: 'Is called when credentials got used for a login.'
operationId: credentialsUsed
parameters:
- name: credentialsUuid
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: OK
content:
'application/json':
schema:
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: 'error-responses.yaml#/components/responses/Forbidden'
@@ -2,7 +2,7 @@ get:
summary: Currently logged in user data.
description: Returns information about the currently logged in user.
tags:
- credentials
- profile
operationId: getCurrentLoginUser
responses:
"200":
@@ -10,7 +10,7 @@ get:
content:
'application/json':
schema:
$ref: 'credentials-schemas.yaml#/components/schemas/CurrentLoginUser'
$ref: 'profile-schemas.yaml#/components/schemas/CurrentLoginUser'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
@@ -13,7 +13,7 @@ components:
globalAdmin:
type: boolean
Credentials:
Profile:
type: object
properties:
uuid:
@@ -40,22 +40,17 @@ components:
type: number
globalGid:
type: number
onboardingToken:
type: string
contexts:
scopes:
type: array
items:
$ref: 'context-schemas.yaml#/components/schemas/Context'
lastUsed:
type: string
format: date-time
$ref: 'scope-schemas.yaml#/components/schemas/Scope'
required:
- uuid
- active
- contexts
- scopes
additionalProperties: false
CredentialsPatch:
ProfilePatch:
type: object
properties:
totpSecrets:
@@ -73,13 +68,13 @@ components:
nullable: true
active:
type: boolean
contexts:
scopes:
type: array
items:
$ref: 'context-schemas.yaml#/components/schemas/Context'
$ref: 'scope-schemas.yaml#/components/schemas/Scope'
additionalProperties: false
CredentialsInsert:
ProfileInsert:
type: object
properties:
person.uuid:
@@ -88,12 +83,6 @@ components:
nickname:
type: string
pattern: '^[a-z][a-z0-9]{1,8}-[a-z0-9]{1,10}$' # TODO.spec: pattern for login nickname
totpSecrets:
type: array
items:
type: string
phonePassword:
type: string
emailAddress:
type: string
smsNumber:
@@ -104,12 +93,16 @@ components:
type: number
globalGid:
type: number
onboardingToken:
phonePassword:
type: string
contexts:
totpSecrets:
type: array
items:
$ref: 'context-schemas.yaml#/components/schemas/Context'
type: string
scopes:
type: array
items:
$ref: 'scope-schemas.yaml#/components/schemas/Scope'
required:
- person.uuid
- nickname
@@ -1,23 +1,23 @@
get:
tags:
- credentials
description: 'Fetch a single credentials its uuid, if visible for the current subject.'
operationId: getSingleCredentialsByUuid
- profile
description: 'Fetch a single profile its uuid, if visible for the current subject.'
operationId: getSingleProfileByUuid
parameters:
- name: credentialsUuid
- name: profileUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the credentials to fetch.
description: UUID of the profile to fetch.
responses:
"200":
description: OK
content:
'application/json':
schema:
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
$ref: 'profile-schemas.yaml#/components/schemas/Profile'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
@@ -26,11 +26,11 @@ get:
patch:
tags:
- credentials
description: 'Updates a single credentials identified by its uuid, if permitted for the current subject.'
operationId: patchCredentials
- profile
description: 'Updates a single profile identified by its uuid, if permitted for the current subject.'
operationId: patchProfile
parameters:
- name: credentialsUuid
- name: profileUuid
in: path
required: true
schema:
@@ -40,14 +40,14 @@ patch:
content:
'application/json':
schema:
$ref: 'credentials-schemas.yaml#/components/schemas/CredentialsPatch'
$ref: 'profile-schemas.yaml#/components/schemas/ProfilePatch'
responses:
"200":
description: OK
content:
'application/json':
schema:
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
$ref: 'profile-schemas.yaml#/components/schemas/Profile'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
@@ -55,17 +55,17 @@ patch:
delete:
tags:
- credentials
description: 'Delete a single credentials identified by its uuid, if permitted for the current subject.'
operationId: deleteCredentialsByUuid
- profile
description: 'Delete a single profile identified by its uuid, if permitted for the current subject.'
operationId: deleteProfileByUuid
parameters:
- name: credentialsUuid
- name: profileUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the credentials to delete.
description: UUID of the profile to delete.
responses:
"204":
description: No Content
@@ -1,9 +1,9 @@
get:
summary: Returns a list of all credentials.
description: Returns the list of all credentials which are visible to the current subject or any of it's assumed roles.
summary: Returns a list of all profile.
description: Returns the list of all profile which are visible to the current subject or any of it's assumed roles.
tags:
- credentials
operationId: getListOfCredentials
- profile
operationId: getListOfProfile
parameters:
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: personUuid
@@ -12,7 +12,7 @@ get:
schema:
type: string
format: uuid
description: The UUID of the person, whose credentials are to be fetched. Or null, if all credentials of the login-use should be fetched.
description: The UUID of the person, whose profile are to be fetched. Or null, if all profile of the login-use should be fetched.
responses:
"200":
description: OK
@@ -21,31 +21,31 @@ get:
schema:
type: array
items:
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
$ref: 'profile-schemas.yaml#/components/schemas/Profile'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: 'error-responses.yaml#/components/responses/Forbidden'
post:
summary: Adds a new credentials.
summary: Adds a new profile.
tags:
- credentials
operationId: postNewCredentials
- profile
operationId: postNewProfile
requestBody:
description: A JSON object describing the new credential.
required: true
content:
application/json:
schema:
$ref: 'credentials-schemas.yaml#/components/schemas/CredentialsInsert'
$ref: 'profile-schemas.yaml#/components/schemas/ProfileInsert'
responses:
"201":
description: Created
content:
'application/json':
schema:
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
$ref: 'profile-schemas.yaml#/components/schemas/Profile'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
@@ -3,7 +3,7 @@ components:
schemas:
Context:
Scope:
type: object
properties:
uuid:
@@ -1,9 +1,9 @@
get:
summary: Returns a list of all accessible contexts.
description: Returns the list of all contexts which are visible to the current subject or any of it's assumed roles.
summary: Returns a list of all accessible scopes.
description: Returns the list of all scopes which are visible to the current subject or any of it's assumed roles.
tags:
- contexts
operationId: getListOfContexts
- scopes
operationId: getListOfScopes
parameters:
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
responses:
@@ -14,7 +14,7 @@ get:
schema:
type: array
items:
$ref: 'context-schemas.yaml#/components/schemas/Context'
$ref: 'scope-schemas.yaml#/components/schemas/Scope'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
+1 -1
View File
@@ -75,7 +75,7 @@ metrics:
# HOWTO set logging-levels for certain Java packages (trace, debug, info, warn, error)
logging:
level:
# org.springframework.security: info
org.springframework.security: info
# org.hibernate.SQL: DEBUG # Enable SQL query logging
# org.hibernate.orm.jdbc.bind: TRACE # Enable SQL parameter binding logging
# org.springframework.web: DEBUG
@@ -31,34 +31,41 @@ end; $$;
--//
-- ============================================================================
--changeset michael.hoennig:rbac-base-SUBJECT endDelimiter:--//
--changeset michael.hoennig:rbac-base-SUBJECT runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM information_schema.tables
WHERE table_schema = 'rbac' AND table_name = 'subject') THEN
*/
create table rbac.subject
(
uuid uuid primary key references rbac.reference (uuid) on delete cascade,
name varchar(63) not null unique
);
CREATE TABLE rbac.subject
(
uuid uuid primary key references rbac.reference (uuid) on delete cascade,
name varchar(63) not null unique
);
call base.create_journal('rbac.subject');
CALL base.create_journal('rbac.subject');
END IF;
END
$$;
create or replace function rbac.create_subject(subjectName varchar)
returns uuid
returns null on null input
language plpgsql as $$
declare
objectId uuid;
stableUuidNamespace uuid;
subjectUuid uuid;
begin
stableUuidNamespace := '6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid;
subjectUuid := uuid_generate_v5(stableUuidNamespace, subjectName);
insert
into rbac.reference (type)
values ('rbac.subject')
returning uuid into objectId;
into rbac.reference (uuid, type)
values (subjectUuid, 'rbac.subject');
insert
into rbac.subject (uuid, name)
values (objectid, subjectName);
return objectId;
values (subjectUuid, subjectName);
return subjectUuid;
end;
$$;
@@ -4,15 +4,15 @@
-- I presume it's a bug in Liquibase that other changeset checksums are changed by new changesets in the same file
-- ============================================================================
--changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATION-FOR-CREDENTIALS context:!without-test-data endDelimiter:--//
--changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATION-FOR-PROFILES context:!without-test-data runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$
begin
call hs_office.person_create_test_data('NP', null,'Hostmaster', 'Alex');
call hs_office.person_create_test_data('NP', null, 'Hostmaster', 'Fran');
call hs_office.person_create_test_data('NP', null, 'User', 'Drew');
call hs_office.person_create_test_data('NP', null, 'User', 'Test');
call hs_office.person_create_test_data('NP', null,'Hostmaster', 'Alex', true);
call hs_office.person_create_test_data('NP', null, 'Hostmaster', 'Fran', true);
call hs_office.person_create_test_data('NP', null, 'User', 'Drew', true);
call hs_office.person_create_test_data('NP', null, 'User', 'Test', true);
end;
$$;
--//
@@ -2,7 +2,7 @@
-- ============================================================================
--changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATOR endDelimiter:--//
--changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATOR runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@@ -12,7 +12,8 @@ create or replace procedure hs_office.person_create_test_data(
newPersonType hs_office.PersonType,
newTradeName varchar,
newFamilyName varchar = null,
newGivenName varchar = null
newGivenName varchar = null,
ignoreIfExists boolean = false
)
language plpgsql as $$
declare
@@ -22,7 +23,10 @@ begin
fullName := concat_ws(', ', newTradeName, newFamilyName, newGivenName);
emailAddr = 'person-' || left(base.cleanIdentifier(fullName), 32) || '@example.com';
call base.defineContext('creating person test-data');
perform rbac.create_subject(emailAddr);
if ignoreIfExists and exists (select 1 from rbac.subject where name = emailAddr) then
return;
end if;
perform rbac.create_subject(emailAddr);call base.defineContext('creating person test-data', null, emailAddr);
call base.defineContext('creating person test-data', null, emailAddr);
raise notice 'creating test person: % by %', fullName, emailAddr;
@@ -2,7 +2,7 @@
-- ============================================================================
--changeset michael.hoennig:hs-credentials-SCHEMA endDelimiter:--//
--changeset michael.hoennig:hs-profile-SCHEMA endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE SCHEMA hs_accounts;
--//
@@ -2,10 +2,10 @@
-- ============================================================================
--changeset michael.hoennig:hs-credentials-CREDENTIALS-TABLE endDelimiter:--//
--changeset michael.hoennig:hs-profile-PROFILE-TABLE endDelimiter:--//
-- ----------------------------------------------------------------------------
create table hs_accounts.credentials
create table hs_accounts.profile
(
uuid uuid PRIMARY KEY references rbac.subject (uuid) initially deferred,
version int not null default 0,
@@ -13,10 +13,8 @@ create table hs_accounts.credentials
person_uuid uuid not null references hs_office.person(uuid),
active bool,
last_used timestamp,
global_uid int unique, -- w/o
global_gid int unique, -- w/o
onboarding_token text, -- w/o, but can be set to null to invalidate
totp_secrets text[],
phone_password text,
@@ -27,10 +25,10 @@ create table hs_accounts.credentials
-- ============================================================================
--changeset michael.hoennig:hs-credentials-context-CONTEXT-TABLE endDelimiter:--//
--changeset michael.hoennig:hs-profile-scope-SCOPE-TABLE endDelimiter:--//
-- ----------------------------------------------------------------------------
create table hs_accounts.context
create table hs_accounts.scope
(
uuid uuid PRIMARY KEY,
version int not null default 0,
@@ -48,31 +46,31 @@ create table hs_accounts.context
-- ============================================================================
--changeset michael.hoennig:hs-credentials-CONTEXT-IMMUTABLE-TRIGGER endDelimiter:--//
--changeset michael.hoennig:hs-profile-SCOPE-IMMUTABLE-TRIGGER endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION hs_accounts.prevent_context_update()
CREATE OR REPLACE FUNCTION hs_accounts.prevent_scope_update()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'Updates to hs_accounts.context are not allowed.';
RAISE EXCEPTION 'Updates to hs_accounts.scope are not allowed.';
END;
$$ LANGUAGE plpgsql;
-- Trigger to enforce immutability
CREATE TRIGGER context_immutable_trigger
BEFORE UPDATE ON hs_accounts.context
FOR EACH ROW EXECUTE FUNCTION hs_accounts.prevent_context_update();
CREATE TRIGGER scope_immutable_trigger
BEFORE UPDATE ON hs_accounts.scope
FOR EACH ROW EXECUTE FUNCTION hs_accounts.prevent_scope_update();
--//
-- ============================================================================
--changeset michael.hoennig:hs_accounts-CONTEXT-MAPPING endDelimiter:--//
--changeset michael.hoennig:hs_accounts-SCOPE-MAPPING endDelimiter:--//
-- ----------------------------------------------------------------------------
create table hs_accounts.context_mapping
create table hs_accounts.scope_mapping
(
uuid uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
credentials_uuid uuid references hs_accounts.credentials(uuid) ON DELETE CASCADE,
context_uuid uuid references hs_accounts.context(uuid) ON DELETE RESTRICT
profile_uuid uuid references hs_accounts.profile(uuid) ON DELETE CASCADE,
scope_uuid uuid references hs_accounts.scope(uuid) ON DELETE RESTRICT
);
--//
@@ -81,16 +79,16 @@ create table hs_accounts.context_mapping
--changeset michael.hoennig:hs-hs_accounts-JOURNALS endDelimiter:--//
-- ----------------------------------------------------------------------------
call base.create_journal('hs_accounts.context_mapping');
call base.create_journal('hs_accounts.context');
call base.create_journal('hs_accounts.credentials');
call base.create_journal('hs_accounts.scope_mapping');
call base.create_journal('hs_accounts.scope');
call base.create_journal('hs_accounts.profile');
--//
-- ============================================================================
--changeset michael.hoennig:hs_accounts-HISTORICIZATION endDelimiter:--//
-- ----------------------------------------------------------------------------
call base.tx_create_historicization('hs_accounts.context_mapping');
call base.tx_create_historicization('hs_accounts.context');
call base.tx_create_historicization('hs_accounts.credentials');
call base.tx_create_historicization('hs_accounts.scope_mapping');
call base.tx_create_historicization('hs_accounts.scope');
call base.tx_create_historicization('hs_accounts.profile');
--//
@@ -1,41 +0,0 @@
### rbac credentialsContext
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph credentialsContext["`**credentialsContext**`"]
direction TB
style credentialsContext fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph credentialsContext:roles[ ]
style credentialsContext:roles fill:#dd4901,stroke:white
role:credentialsContext:OWNER[[credentialsContext:OWNER]]
role:credentialsContext:ADMIN[[credentialsContext:ADMIN]]
role:credentialsContext:REFERRER[[credentialsContext:REFERRER]]
end
subgraph credentialsContext:permissions[ ]
style credentialsContext:permissions fill:#dd4901,stroke:white
perm:credentialsContext:INSERT{{credentialsContext:INSERT}}
perm:credentialsContext:UPDATE{{credentialsContext:UPDATE}}
perm:credentialsContext:DELETE{{credentialsContext:DELETE}}
perm:credentialsContext:SELECT{{credentialsContext:SELECT}}
end
end
%% granting roles to roles
role:credentialsContext:OWNER ==> role:credentialsContext:ADMIN
role:credentialsContext:ADMIN ==> role:credentialsContext:REFERRER
%% granting permissions to roles
role:rbac.global:ADMIN ==> perm:credentialsContext:INSERT
role:rbac.global:ADMIN ==> perm:credentialsContext:UPDATE
role:rbac.global:ADMIN ==> perm:credentialsContext:DELETE
role:rbac.global:REFERRER ==> perm:credentialsContext:SELECT
```
@@ -0,0 +1,41 @@
### rbac profileContext
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph profileContext["`**profileContext**`"]
direction TB
style profileContext fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph profileContext:roles[ ]
style profileContext:roles fill:#dd4901,stroke:white
role:profileContext:OWNER[[profileContext:OWNER]]
role:profileContext:ADMIN[[profileContext:ADMIN]]
role:profileContext:REFERRER[[profileContext:REFERRER]]
end
subgraph profileContext:permissions[ ]
style profileContext:permissions fill:#dd4901,stroke:white
perm:profileContext:INSERT{{profileContext:INSERT}}
perm:profileContext:UPDATE{{profileContext:UPDATE}}
perm:profileContext:DELETE{{profileContext:DELETE}}
perm:profileContext:SELECT{{profileContext:SELECT}}
end
end
%% granting roles to roles
role:profileContext:OWNER ==> role:profileContext:ADMIN
role:profileContext:ADMIN ==> role:profileContext:REFERRER
%% granting permissions to roles
role:rbac.global:ADMIN ==> perm:profileContext:INSERT
role:rbac.global:ADMIN ==> perm:profileContext:UPDATE
role:rbac.global:ADMIN ==> perm:profileContext:DELETE
role:rbac.global:REFERRER ==> perm:profileContext:SELECT
```
@@ -2,7 +2,7 @@
-- ============================================================================
--changeset michael.hoennig:hs_accounts-credentials-TEST-DATA context:!without-test-data endDelimiter:--//
--changeset michael.hoennig:hs_accounts-profile-TEST-DATA context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$
@@ -16,11 +16,11 @@ declare
personDrewUuid uuid;
context_HSADMIN_prod hs_accounts.context;
context_SSH_internal hs_accounts.context;
context_SSH_external hs_accounts.context;
context_MATRIX_internal hs_accounts.context;
context_MATRIX_external hs_accounts.context;
scope_HSADMIN_prod hs_accounts.scope;
scope_SSH_internal hs_accounts.scope;
scope_SSH_external hs_accounts.scope;
scope_MATRIX_internal hs_accounts.scope;
scope_MATRIX_external hs_accounts.scope;
begin
call base.defineContext('creating booking-project test-data', null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN');
@@ -32,28 +32,28 @@ begin
userDrewSubjectUuid = (SELECT uuid FROM rbac.subject WHERE name='selfregistered-user-drew@hostsharing.org');
personDrewUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Drew');
-- Add test contexts
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
-- Add test scopes
INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod', true, true)
RETURNING * INTO context_HSADMIN_prod;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
RETURNING * INTO scope_HSADMIN_prod;
INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('22222222-2222-2222-2222-222222222222', 'SSH', 'internal', true, false)
RETURNING * INTO context_SSH_internal;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
RETURNING * INTO scope_SSH_internal;
INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('33333333-3333-3333-3333-333333333333', 'SSH', 'external', false, true)
RETURNING * INTO context_SSH_external;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
RETURNING * INTO scope_SSH_external;
INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('44444444-4444-4444-4444-444444444444', 'MATRIX', 'internal', true, false)
RETURNING * INTO context_MATRIX_internal;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
RETURNING * INTO scope_MATRIX_internal;
INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('55555555-5555-5555-5555-555555555555', 'MATRIX', 'external', true, true)
RETURNING * INTO context_MATRIX_external;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
RETURNING * INTO scope_MATRIX_external;
INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('66666666-6666-6666-6666-666666666666', 'MASTODON', 'external', false, true);
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('77777777-7777-7777-7777-777777777777', 'BBB', 'external', false, true);
-- grant general access to public credential contexts
-- grant general access to public credential scopes
-- TODO_impl: RBAC rules for _rv do not yet work properly
-- call rbac.grantPermissiontoRole(
-- rbac.createPermission(context_HSADMIN_prod.uuid, 'SELECT'),
@@ -64,25 +64,25 @@ begin
-- call rbac.grantPermissionToRole(
-- rbac.createPermission(context_MATRIX_internal.uuid, 'SELECT'),
-- rbac.global_ADMIN());
-- call rbac.grantRoleToRole(hs_accounts.context_REFERRER(context_SSH_internal), rbac.global_ADMIN());
-- call rbac.grantRoleToRole(hs_accounts.context_REFERRER(context_MATRIX_internal), rbac.global_ADMIN());
-- call rbac.grantRoleToRole(hs_accounts.scope_REFERRER(context_SSH_internal), rbac.global_ADMIN());
-- call rbac.grantRoleToRole(hs_accounts.scope_REFERRER(context_MATRIX_internal), rbac.global_ADMIN());
-- Add test credentials (linking to assumed rbac.subject UUIDs)
INSERT INTO hs_accounts.credentials (uuid, version, person_uuid, active, global_uid, global_gid, onboarding_token, totp_secrets, phone_password, email_address, sms_number) VALUES
( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, 'token-abc', ARRAY['otp-secret-1a', 'otp-secret-1b'], 'phone-pw-1', 'alex@example.com', '111-222-3333'),
( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, 'token-def', ARRAY['otp-secret-2'], 'phone-pw-2', 'fran@example.com', '444-555-6666'),
( userDrewSubjectUuid, 0, personDrewUuid, true, 1003, 1003, 'token-def', ARRAY['otp-secret-3'], 'phone-pw-3', 'drew@example.org', '999-888-7777');
-- Add test profile (linking to assumed rbac.subject UUIDs)
INSERT INTO hs_accounts.profile (uuid, version, person_uuid, active, global_uid, global_gid, totp_secrets, phone_password, email_address, sms_number) VALUES
( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, ARRAY['otp-secret-1a', 'otp-secret-1b'], 'phone-pw-1', 'alex@example.com', '111-222-3333'),
( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, ARRAY['otp-secret-2'], 'phone-pw-2', 'fran@example.com', '444-555-6666'),
( userDrewSubjectUuid, 0, personDrewUuid, true, 1003, 1003, ARRAY['otp-secret-3'], 'phone-pw-3', 'drew@example.org', '999-888-7777');
-- Map credentials to contexts
INSERT INTO hs_accounts.context_mapping (credentials_uuid, context_uuid) VALUES
(superuserAlexSubjectUuid, context_HSADMIN_prod.uuid),
(superuserFranSubjectUuid, context_HSADMIN_prod.uuid),
(userDrewSubjectUuid, context_HSADMIN_prod.uuid),
(superuserAlexSubjectUuid, context_SSH_internal.uuid),
(superuserFranSubjectUuid, context_SSH_internal.uuid),
(userDrewSubjectUuid, context_SSH_external.uuid),
(superuserAlexSubjectUuid, context_MATRIX_internal.uuid),
(superuserFranSubjectUuid, context_MATRIX_internal.uuid);
-- Map profile to contexts
INSERT INTO hs_accounts.scope_mapping (profile_uuid, scope_uuid) VALUES
(superuserAlexSubjectUuid, scope_HSADMIN_prod.uuid),
(superuserFranSubjectUuid, scope_HSADMIN_prod.uuid),
(userDrewSubjectUuid, scope_HSADMIN_prod.uuid),
(superuserAlexSubjectUuid, scope_SSH_internal.uuid),
(superuserFranSubjectUuid, scope_SSH_internal.uuid),
(userDrewSubjectUuid, scope_SSH_external.uuid),
(superuserAlexSubjectUuid, scope_MATRIX_internal.uuid),
(superuserFranSubjectUuid, scope_MATRIX_internal.uuid);
end; $$;
--//
+6 -10
View File
@@ -5,21 +5,17 @@ test.pinged--in-your-language=pinged - auf Deutsch
test.ponged-{0}--in-your-language=ponged {0} - auf Deutsch
test.available-in-all-properties-files=Hallo {0} - DE!
# authorization
auth.cas-service-ticket-could-not-be-verified=CAS Service-Ticket konnte nicht verifiziert werden
auth.unknown-authorization-ticket=unbekanntes Autorisierungs-Ticket
# general validations
general.{0}-{1}-not-found={0} "{1}" nicht gefunden
general.{0}-{1}-not-found-or-not-accessible={0} "{1}" nicht gefunden oder nicht zugänglich
general.but-is=ist aber
# credentials validations
credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1}
credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=Zugriff verweigert: personUuid "{0}" wird von der eingeloggten Person nicht repräsentiert
credentials.access-denied-for-contexts-{0}=Kontext-Zugriff verweigert: {0}
credentials.context-requires-natural-person-{0}=Kontext verlangt eine natürliche Person: {0}
credentials.own-hsadmin-credentials-must-not-be-removed=die eigenen hsadmin-Credentials dürfen nicht entfernt werden
# profile validations
profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}=existierender Gültigkeitsbereich {0} passt nicht zum angegebenen {1}
profile.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=Zugriff verweigert: personUuid "{0}" wird von der eingeloggten Person nicht repräsentiert
profile.access-denied-for-scopes-{0}=Zugriff auf Geltungsbereich verweigert: {0}
profile.scope-requires-natural-person-{0}=Geltungsbereich verlangt eine natürliche Person: {0}
profile.own-hsadmin-profile-must-not-be-removed=die eigenen hsadmin-Profile dürfen nicht entfernt werden
# office.coop-shares
office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1}
+6 -10
View File
@@ -5,21 +5,17 @@ test.pinged--in-your-language=pinged - in English
test.ponged-{0}--in-your-language=ponged {0} - in English
test.available-in-all-properties-files=Hello {0} - EN!
# authorization
auth.cas-service-ticket-could-not-be-verified=CAS service-ticket could not be verified
auth.unknown-authorization-ticket=unknown authorization ticket
# general validations
general.{0}-{1}-not-found={0} "{1}" not found
general.{0}-{1}-not-found-or-not-accessible={0} "{1}" not found or not accessible
general.but-is=but is
# credentials validations
credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}=existing {0} does not match given resource {1}
credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=access denied: personUuid "{0}" not represented by currently logged in person
credentials.access-denied-for-contexts-{0}=context-access denied: {0}
credentials.context-requires-natural-person-{0}=context requires natural person: {0}
credentials.own-hsadmin-credentials-must-not-be-removed=own hsadmin-credentials must not be removed
# profile validations
profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}=existing {0} does not match given resource {1}
profile.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=access denied: personUuid "{0}" not represented by currently logged in person
profile.access-denied-for-scopes-{0}=scope-access denied: {0}
profile.scope-requires-natural-person-{0}=scope requires natural person: {0}
profile.own-hsadmin-profile-must-not-be-removed=own hsadmin-profile must not be removed
# office.coop-shares
office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=for transactiontType {0} shareCount must be positive but is {1}
+6 -10
View File
@@ -5,21 +5,17 @@
test.pinged--in-your-language=ponged {0} - en Francais
test.available-in-all-properties-files=Salut {0} - FR!
# authorization
auth.cas-service-ticket-could-not-be-verified=Le ticket de service CAS n'a pas pu être vérifié
auth.unknown-authorization-ticket=ticket d'autorisation inconnu
# general validations
general.{0}-{1}-not-found={0} "{1}" non trouvé
general.{0}-{1}-not-found-or-not-accessible={0} "{1}" non trouvé ou non accessible
general.but-is=mais c'est
# credentials validations
credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}={0} existant ne correspond pas à la ressource donnée {1}
credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=accès refusé : personUuid "{0}" non représenté par la personne actuellement connectée
credentials.access-denied-for-contexts-{0}=accès au contexte refusé : {0}
credentials.context-requires-natural-person-{0}=le contexte requiert une personne physique : {0}
credentials.own-hsadmin-credentials-must-not-be-removed=suppression des identifiants hsadmin propres interdite
# profile validations
profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}={0} existant ne correspond pas à la ressource donnée {1}
profile.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=accès refusé : personUuid "{0}" non représenté par la personne actuellement connectée
profile.access-denied-for-scopes-{0}=accès au domaine d'application refusé : {0}
profile.scope-requires-natural-person-{0}=le domaine d'application requiert une personne physique : {0}
profile.own-hsadmin-profile-must-not-be-removed=suppression des identifiants hsadmin propres interdite
# office.coop-shares
office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=pour le type de transaction {0}, shareCount doit être positif mais est {1}