login-credentials without RBAC (#173)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/173 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||
import net.hostsharing.hsadminng.repr.Stringify;
|
||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@SuperBuilder(builderMethodName = "baseBuilder", toBuilder = true)
|
||||
@MappedSuperclass
|
||||
public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<HsCredentialsContext> {
|
||||
|
||||
private static Stringify<HsCredentialsContext> stringify = stringify(HsCredentialsContext.class, "loginContext")
|
||||
.withProp(HsCredentialsContext::getType)
|
||||
.withProp(HsCredentialsContext::getQualifier)
|
||||
.quotedValues(false)
|
||||
.withSeparator(":");
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
@Column(name = "uuid", nullable = false, updatable = false)
|
||||
private UUID uuid;
|
||||
|
||||
@NotNull
|
||||
@Column
|
||||
private int version;
|
||||
|
||||
@NotNull
|
||||
@Column(name = "type", length = 16)
|
||||
private String type;
|
||||
|
||||
@Column(name = "qualifier", length = 80)
|
||||
private String qualifier;
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
import jakarta.persistence.AttributeOverride;
|
||||
import jakarta.persistence.AttributeOverrides;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
|
||||
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.GUEST;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.REFERRER;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.WITHOUT_IMPLICIT_GRANTS;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_credentials", name = "context") // TODO_impl: RBAC rules for _rv do not yet work properly
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(name = "uuid", column = @Column(name = "uuid"))
|
||||
})
|
||||
public class HsCredentialsContextRbacEntity extends HsCredentialsContext {
|
||||
|
||||
// TODO_impl: RBAC rules for _rv do not yet work properly (remove the X)
|
||||
public static RbacSpec rbacX() {
|
||||
return rbacViewFor("credentialsContext", HsCredentialsContextRbacEntity.class)
|
||||
.withIdentityView(SQL.projection("type || ':' || qualifier"))
|
||||
.withRestrictedViewOrderBy(SQL.expression("type || ':' || qualifier"))
|
||||
.withoutUpdatableColumns()
|
||||
.createRole(OWNER, WITHOUT_IMPLICIT_GRANTS)
|
||||
.createSubRole(ADMIN, WITHOUT_IMPLICIT_GRANTS)
|
||||
.createSubRole(REFERRER, WITHOUT_IMPLICIT_GRANTS)
|
||||
.toRole(GLOBAL, ADMIN).grantPermission(INSERT)
|
||||
.toRole(GLOBAL, ADMIN).grantPermission(DELETE)
|
||||
.toRole(GLOBAL, GUEST).grantPermission(SELECT);
|
||||
}
|
||||
|
||||
// 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-credentials/9513-hs-credentials-rbac");
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
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);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
import jakarta.persistence.AttributeOverride;
|
||||
import jakarta.persistence.AttributeOverrides;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_credentials", name = "context")
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(name = "uuid", column = @Column(name = "uuid"))
|
||||
})
|
||||
public class HsCredentialsContextRealEntity extends HsCredentialsContext {
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
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);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.api.LoginContextsApi;
|
||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextResource;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class HsCredentialsContextsController implements LoginContextsApi {
|
||||
|
||||
@Autowired
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsCredentialsContextRbacRepository contextRepo;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<List<LoginContextResource>> getListOfLoginContexts(final String assumedRoles) {
|
||||
context.assumeRoles(assumedRoles);
|
||||
|
||||
final var loginContexts = contextRepo.findAll();
|
||||
final var result = mapper.mapList(loginContexts, LoginContextResource.class);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.api.LoginCredentialsApi;
|
||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsInsertResource;
|
||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsPatchResource;
|
||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsResource;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class HsCredentialsController implements LoginCredentialsApi {
|
||||
|
||||
@Autowired
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerWrapper em;
|
||||
|
||||
@Autowired
|
||||
private StrictMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePersonRbacRepository personRepo;
|
||||
|
||||
@Autowired
|
||||
private HsCredentialsRepository loginCredentialsRepo;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<LoginCredentialsResource> getSingleLoginCredentialsByUuid(
|
||||
final String assumedRoles,
|
||||
final UUID loginCredentialsUuid) {
|
||||
context.assumeRoles(assumedRoles);
|
||||
|
||||
final var credentials = loginCredentialsRepo.findByUuid(loginCredentialsUuid);
|
||||
final var result = mapper.map(credentials, LoginCredentialsResource.class);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<List<LoginCredentialsResource>> getListOfLoginCredentialsByPersonUuid(
|
||||
final String assumedRoles,
|
||||
final UUID personUuid
|
||||
) {
|
||||
context.assumeRoles(assumedRoles);
|
||||
|
||||
final var person = personRepo.findByUuid(personUuid).orElseThrow(); // FIXME: use proper exception
|
||||
final var credentials = loginCredentialsRepo.findByPerson(person);
|
||||
final var result = mapper.mapList(credentials, LoginCredentialsResource.class);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<LoginCredentialsResource> postNewLoginCredentials(
|
||||
final String assumedRoles,
|
||||
final LoginCredentialsInsertResource body
|
||||
) {
|
||||
context.assumeRoles(assumedRoles);
|
||||
|
||||
final var newLoginCredentialsEntity = mapper.map(body, HsCredentialsEntity.class);
|
||||
final var savedLoginCredentialsEntity = loginCredentialsRepo.save(newLoginCredentialsEntity);
|
||||
final var newLoginCredentialsResource = mapper.map(savedLoginCredentialsEntity, LoginCredentialsResource.class);
|
||||
return ResponseEntity.ok(newLoginCredentialsResource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> deleteLoginCredentialsByUuid(final String assumedRoles, final UUID loginCredentialsUuid) {
|
||||
context.assumeRoles(assumedRoles);
|
||||
final var loginCredentialsEntity = em.getReference(HsCredentialsEntity.class, loginCredentialsUuid);
|
||||
em.remove(loginCredentialsEntity);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<LoginCredentialsResource> patchLoginCredentials(
|
||||
final String assumedRoles,
|
||||
final UUID loginCredentialsUuid,
|
||||
final LoginCredentialsPatchResource body
|
||||
) {
|
||||
context.assumeRoles(assumedRoles);
|
||||
|
||||
final var current = loginCredentialsRepo.findByUuid(loginCredentialsUuid).orElseThrow();
|
||||
|
||||
new HsCredentialsEntityPatcher(em, current).apply(body);
|
||||
|
||||
final var saved = loginCredentialsRepo.save(current);
|
||||
final var mapped = mapper.map(saved, LoginCredentialsResource.class);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||
import net.hostsharing.hsadminng.persistence.BaseEntity; // Assuming BaseEntity exists
|
||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||
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.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static jakarta.persistence.CascadeType.MERGE;
|
||||
import static jakarta.persistence.CascadeType.REFRESH;
|
||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(schema = "hs_credentials", name = "credentials")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Stringifyable {
|
||||
|
||||
protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "loginCredentials")
|
||||
.withProp(HsCredentialsEntity::isActive)
|
||||
.withProp(HsCredentialsEntity::getEmailAddress)
|
||||
.withProp(HsCredentialsEntity::getTwoFactorAuth)
|
||||
.withProp(HsCredentialsEntity::getPhonePassword)
|
||||
.withProp(HsCredentialsEntity::getSmsNumber)
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
private UUID uuid;
|
||||
|
||||
@MapsId
|
||||
@OneToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "uuid", nullable = false, updatable = false, referencedColumnName = "uuid")
|
||||
private RbacSubjectEntity subject;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "person_uuid", nullable = false, updatable = false, referencedColumnName = "uuid")
|
||||
private HsOfficePersonRealEntity person;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column
|
||||
private boolean active;
|
||||
|
||||
@Column
|
||||
private Integer globalUid;
|
||||
|
||||
@Column
|
||||
private Integer globalGid;
|
||||
|
||||
@Column
|
||||
private String onboardingToken;
|
||||
|
||||
@Column
|
||||
private String twoFactorAuth;
|
||||
|
||||
@Column
|
||||
private String phonePassword;
|
||||
|
||||
@Column
|
||||
private String emailAddress;
|
||||
|
||||
@Column
|
||||
private String smsNumber;
|
||||
|
||||
@OneToMany(fetch = FetchType.LAZY, cascade = { MERGE, REFRESH }, orphanRemoval = true)
|
||||
@JoinTable(
|
||||
name = "context_mapping", schema = "hs_credentials",
|
||||
joinColumns = @JoinColumn(name = "credentials_uuid", referencedColumnName = "uuid"),
|
||||
inverseJoinColumns = @JoinColumn(name = "context_uuid", referencedColumnName = "uuid")
|
||||
)
|
||||
private Set<HsCredentialsContextRealEntity> loginContexts;
|
||||
|
||||
public Set<HsCredentialsContextRealEntity> getLoginContexts() {
|
||||
if ( loginContexts == null ) {
|
||||
loginContexts = new HashSet<>();
|
||||
}
|
||||
return loginContexts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return active + ":" + emailAddress + ":" + globalUid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextResource;
|
||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsPatchResource;
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class HsCredentialsEntityPatcher implements EntityPatcher<LoginCredentialsPatchResource> {
|
||||
|
||||
private final EntityManager em;
|
||||
private final HsCredentialsEntity entity;
|
||||
|
||||
public HsCredentialsEntityPatcher(final EntityManager em, final HsCredentialsEntity entity) {
|
||||
this.em = em;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(final LoginCredentialsPatchResource resource) {
|
||||
if ( resource.getActive() != null ) {
|
||||
entity.setActive(resource.getActive());
|
||||
}
|
||||
OptionalFromJson.of(resource.getEmailAddress())
|
||||
.ifPresent(entity::setEmailAddress);
|
||||
OptionalFromJson.of(resource.getTwoFactorAuth())
|
||||
.ifPresent(entity::setTwoFactorAuth);
|
||||
OptionalFromJson.of(resource.getSmsNumber())
|
||||
.ifPresent(entity::setSmsNumber);
|
||||
OptionalFromJson.of(resource.getPhonePassword())
|
||||
.ifPresent(entity::setPhonePassword);
|
||||
if (resource.getContexts() != null) {
|
||||
syncLoginContextEntities(resource.getContexts(), entity.getLoginContexts());
|
||||
}
|
||||
}
|
||||
|
||||
public void syncLoginContextEntities(
|
||||
List<LoginContextResource> resources,
|
||||
Set<HsCredentialsContextRealEntity> entities
|
||||
) {
|
||||
final var resourceUuids = resources.stream()
|
||||
.map(LoginContextResource::getUuid)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
final var entityUuids = entities.stream()
|
||||
.map(HsCredentialsContextRealEntity::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 ) {
|
||||
// FIXME: i18n
|
||||
throw new EntityNotFoundException(
|
||||
HsCredentialsContextRealEntity.class.getName() + " with uuid " + resource.getUuid() + " not found.");
|
||||
}
|
||||
if (!existingContextEntity.getType().equals(resource.getType().name()) &&
|
||||
!existingContextEntity.getQualifier().equals(resource.getQualifier())) {
|
||||
// FIXME: i18n
|
||||
throw new EntityNotFoundException("existing " + existingContextEntity + " does not match given resource " + resource);
|
||||
}
|
||||
entities.add(existingContextEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.hostsharing.hsadminng.credentials;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
|
||||
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.save")
|
||||
HsCredentialsEntity save(final HsCredentialsEntity entity);
|
||||
}
|
||||
+12
-10
@@ -4,6 +4,7 @@ package net.hostsharing.hsadminng.rbac.generator;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.indented;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
|
||||
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.withQuoted;
|
||||
|
||||
public class RbacRestrictedViewGenerator {
|
||||
private final RbacSpec rbacDef;
|
||||
@@ -22,20 +23,21 @@ public class RbacRestrictedViewGenerator {
|
||||
--changeset RbacRestrictedViewGenerator:${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call rbac.generateRbacRestrictedView('${rawTableName}',
|
||||
$orderBy$
|
||||
${orderBy}
|
||||
$orderBy$,
|
||||
$updates$
|
||||
${updates}
|
||||
$updates$);
|
||||
${orderBy},
|
||||
${updates}
|
||||
);
|
||||
--//
|
||||
|
||||
""",
|
||||
with("liquibaseTagPrefix", liquibaseTagPrefix),
|
||||
with("orderBy", indented(2, rbacDef.getOrderBySqlExpression().sql)),
|
||||
with("updates", indented(2, rbacDef.getUpdatableColumns().stream()
|
||||
.map(c -> c + " = new." + c)
|
||||
.collect(joining(",\n")))),
|
||||
withQuoted("orderBy", indented(2, rbacDef.getOrderBySqlExpression().sql)),
|
||||
withQuoted("updates",
|
||||
rbacDef.getUpdatableColumns().isEmpty()
|
||||
? null
|
||||
: indented(2, rbacDef.getUpdatableColumns().stream()
|
||||
.map(c -> c + " = new." + c)
|
||||
.collect(joining(",\n")))
|
||||
),
|
||||
with("rawTableName", rawTableName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ public class RbacSpec {
|
||||
public static final String GLOBAL = "rbac.global";
|
||||
public static final String OUTPUT_BASEDIR = "src/main/resources/db/changelog";
|
||||
|
||||
public static Consumer<RbacSpec.RbacRoleDefinition> WITHOUT_IMPLICIT_GRANTS = with -> {};
|
||||
|
||||
private final EntityAlias rootEntityAlias;
|
||||
|
||||
private final Set<RbacSubjectReference> userDefs = new LinkedHashSet<>();
|
||||
@@ -109,12 +111,23 @@ public class RbacSpec {
|
||||
* @return
|
||||
* the `this` instance itself to allow chained calls.
|
||||
*/
|
||||
public RbacSpec withUpdatableColumns(final String... columnNames) {
|
||||
public RbacSpec withUpdatableColumns(final String columnName, final String... columnNames) {
|
||||
Collections.addAll(updatableColumns, columnName);
|
||||
Collections.addAll(updatableColumns, columnNames);
|
||||
verifyVersionColumnExists();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies, thta no columns are updatable at all.
|
||||
*
|
||||
* @return this
|
||||
*/
|
||||
public RbacSpec withoutUpdatableColumns() {
|
||||
updatableColumns.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Specifies the SQL query which creates the identity view for this entity.
|
||||
*
|
||||
* <p>An identity view is a view which maps an objectUuid to an idName.
|
||||
@@ -188,7 +201,6 @@ public class RbacSpec {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specifies that the given role (OWNER, ADMIN, ...) is to be created for new/updated roles in this table,
|
||||
* which is becomes sub-role of the previously created role.
|
||||
|
||||
+5
@@ -99,6 +99,11 @@ class RolesGrantsAndPermissionsGenerator {
|
||||
.distinct()
|
||||
.map(columnName -> "NEW." + columnName + " is distinct from OLD." + columnName)
|
||||
.collect(joining( "\n or "));
|
||||
if (updateConditions.isBlank()) {
|
||||
// TODO_impl: RBAC rules for _rv do not yet work properly - check if this comment appears in generated output!
|
||||
plPgSql.writeLn("-- no updatable columns found, skipping update trigger");
|
||||
return;
|
||||
}
|
||||
plPgSql.writeLn("""
|
||||
/*
|
||||
Called from the AFTER UPDATE TRIGGER to re-wire the grants.
|
||||
|
||||
@@ -11,7 +11,11 @@ public class StringWriter {
|
||||
private int indentLevel = 0;
|
||||
|
||||
static VarDef with(final String var, final String name) {
|
||||
return new VarDef(var, name);
|
||||
return new VarDef(var, name, false);
|
||||
}
|
||||
|
||||
static VarDef withQuoted(final String var, final String name) {
|
||||
return new VarDef(var, name, true);
|
||||
}
|
||||
|
||||
void writeLn(final String text) {
|
||||
@@ -97,7 +101,7 @@ public class StringWriter {
|
||||
return indented(indentLevel, text);
|
||||
}
|
||||
|
||||
record VarDef(String name, String value){}
|
||||
record VarDef(String name, String value, boolean quoted) {}
|
||||
|
||||
private static final class VarReplacer {
|
||||
|
||||
@@ -111,13 +115,22 @@ public class StringWriter {
|
||||
String apply(final String textToAppend) {
|
||||
text = textToAppend;
|
||||
stream(varDefs).forEach(varDef -> {
|
||||
// TODO.impl: I actually want a case-independent search+replace but ...
|
||||
// for which the substitution String can contain sequences of "${...}" to be replaced by further varDefs.
|
||||
text = text.replace("${" + varDef.name() + "}", varDef.value());
|
||||
text = text.replace("${" + varDef.name().toUpperCase() + "}", varDef.value());
|
||||
text = text.replace("${" + varDef.name().toLowerCase() + "}", varDef.value());
|
||||
replacePlaceholder(varDef.name(),
|
||||
varDef.quoted()
|
||||
? varDef.value() != null
|
||||
? "$" + varDef.name() + "$\n" + varDef.value() + "\n$" + varDef.name() + "$"
|
||||
: "null"
|
||||
: varDef.value());
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
private void replacePlaceholder(final String name, final String value) {
|
||||
// TODO.impl: I actually want a case-independent search+replace but ...
|
||||
// for which the substitution String can contain sequences of "${...}" to be replaced by further varDefs.
|
||||
text = text.replace("${" + name+ "}", value);
|
||||
text = text.replace("${" + name.toUpperCase() + "}", value);
|
||||
text = text.replace("${" + name.toLowerCase() + "}", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user