mirror of https://github.com/halo-dev/halo
refactor: auth provider sorting logic for better maintainability and clarity (#6846)
* refactor: auth provider sorting logic for better maintainability and clarity * Refine UI * chore: remove other auth type * Remove other auth providers --------- Co-authored-by: Ryan Wang <i@ryanc.cc>pull/6860/head v2.20.0
parent
4059f159b2
commit
82498dcedf
|
@ -16034,6 +16034,7 @@
|
|||
},
|
||||
"AuthProviderSpec": {
|
||||
"required": [
|
||||
"authType",
|
||||
"authenticationUrl",
|
||||
"displayName"
|
||||
],
|
||||
|
@ -16072,10 +16073,6 @@
|
|||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"rememberMeSupport": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@ -18297,6 +18294,13 @@
|
|||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"FORM",
|
||||
"OAUTH2"
|
||||
]
|
||||
},
|
||||
"authenticationUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -18324,6 +18328,10 @@
|
|||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"privileged": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
@ -3568,6 +3568,7 @@
|
|||
},
|
||||
"AuthProviderSpec": {
|
||||
"required": [
|
||||
"authType",
|
||||
"authenticationUrl",
|
||||
"displayName"
|
||||
],
|
||||
|
@ -3606,10 +3607,6 @@
|
|||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"rememberMeSupport": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@ -4435,6 +4432,13 @@
|
|||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"FORM",
|
||||
"OAUTH2"
|
||||
]
|
||||
},
|
||||
"authenticationUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -4462,6 +4466,10 @@
|
|||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"privileged": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
@ -8666,6 +8666,7 @@
|
|||
},
|
||||
"AuthProviderSpec": {
|
||||
"required": [
|
||||
"authType",
|
||||
"authenticationUrl",
|
||||
"displayName"
|
||||
],
|
||||
|
@ -8704,10 +8705,6 @@
|
|||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"rememberMeSupport": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
@ -6,7 +6,9 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
|
@ -55,19 +57,23 @@ public class AuthProvider extends AbstractExtension {
|
|||
/**
|
||||
* Auth type: form or oauth2.
|
||||
*/
|
||||
private AuthType authType;
|
||||
@Getter(onMethod_ = @NonNull)
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private AuthType authType = AuthType.OAUTH2;
|
||||
|
||||
private String bindingUrl;
|
||||
|
||||
private String unbindUrl;
|
||||
|
||||
private int priority;
|
||||
|
||||
@Schema(requiredMode = NOT_REQUIRED)
|
||||
private SettingRef settingRef;
|
||||
|
||||
@Schema(requiredMode = NOT_REQUIRED)
|
||||
private ConfigMapRef configMapRef;
|
||||
|
||||
public void setAuthType(AuthType authType) {
|
||||
this.authType = (authType == null ? AuthType.OAUTH2 : authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
@ -89,7 +95,6 @@ public class AuthProvider extends AbstractExtension {
|
|||
|
||||
public enum AuthType {
|
||||
FORM,
|
||||
OAUTH2,
|
||||
;
|
||||
OAUTH2
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ public class Unstructured implements Extension {
|
|||
return new UnstructuredMetadata();
|
||||
}
|
||||
|
||||
@EqualsAndHashCode(exclude = "version")
|
||||
@EqualsAndHashCode(exclude = "tatersion")
|
||||
class UnstructuredMetadata implements MetadataOperator {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2,8 +2,9 @@ package run.halo.app.infra;
|
|||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import run.halo.app.extension.ConfigMap;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
@ -113,7 +114,15 @@ public class SystemSetting {
|
|||
@Data
|
||||
public static class AuthProvider {
|
||||
public static final String GROUP = "authProvider";
|
||||
private Set<String> enabled;
|
||||
private List<AuthProviderState> states;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class AuthProviderState {
|
||||
private String name;
|
||||
private boolean enabled;
|
||||
private int priority;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,6 +19,9 @@ public interface AuthProviderService {
|
|||
|
||||
Mono<List<ListedAuthProvider>> listAll();
|
||||
|
||||
/**
|
||||
* Return a list of enabled AuthProviders sorted by priority.
|
||||
*/
|
||||
Flux<AuthProvider> getEnabledProviders();
|
||||
|
||||
}
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
package run.halo.app.security;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.core.extension.AuthProvider;
|
||||
import run.halo.app.core.extension.UserConnection;
|
||||
import run.halo.app.extension.ConfigMap;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
|
@ -42,17 +46,17 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
|||
@Override
|
||||
public Mono<AuthProvider> enable(String name) {
|
||||
return client.get(AuthProvider.class, name)
|
||||
.flatMap(authProvider -> updateAuthProviderEnabled(enabled -> enabled.add(name))
|
||||
.flatMap(authProvider -> updateAuthProviderEnabled(name, true)
|
||||
.thenReturn(authProvider)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<AuthProvider> disable(String name) {
|
||||
// privileged auth provider cannot be disabled
|
||||
return client.get(AuthProvider.class, name)
|
||||
// privileged auth provider cannot be disabled
|
||||
.filter(authProvider -> !privileged(authProvider))
|
||||
.flatMap(authProvider -> updateAuthProviderEnabled(enabled -> enabled.remove(name))
|
||||
.flatMap(authProvider -> updateAuthProviderEnabled(name, false)
|
||||
.thenReturn(authProvider)
|
||||
);
|
||||
}
|
||||
|
@ -62,50 +66,91 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
|||
var listOptions = ListOptions.builder()
|
||||
.andQuery(ExtensionUtil.notDeleting())
|
||||
.build();
|
||||
return client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())
|
||||
.map(this::convertTo)
|
||||
.collectList()
|
||||
.flatMap(providers -> listMyConnections()
|
||||
.map(connection -> connection.getSpec().getRegistrationId())
|
||||
var allProvidersMono =
|
||||
client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())
|
||||
.map(this::convertTo)
|
||||
.collectList()
|
||||
.map(connectedNames -> providers.stream()
|
||||
.peek(provider -> {
|
||||
boolean isBound = connectedNames.contains(provider.getName());
|
||||
provider.setIsBound(isBound);
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
|
||||
var boundProvidersMono = listMyConnections()
|
||||
.map(connection -> connection.getSpec().getRegistrationId())
|
||||
.collect(Collectors.toSet())
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
|
||||
return Mono.zip(allProvidersMono, boundProvidersMono, fetchProviderStates())
|
||||
.map(tuple3 -> {
|
||||
var allProviders = tuple3.getT1();
|
||||
var boundProviderNames = tuple3.getT2();
|
||||
var stateMap = tuple3.getT3().stream()
|
||||
.collect(Collectors.toMap(SystemSetting.AuthProviderState::getName,
|
||||
Function.identity()));
|
||||
return allProviders.stream()
|
||||
.peek(authProvider -> {
|
||||
authProvider.setIsBound(
|
||||
boundProviderNames.contains(authProvider.getName()));
|
||||
authProvider.setEnabled(false);
|
||||
// set enabled state and priority
|
||||
var state = stateMap.get(authProvider.getName());
|
||||
if (state != null) {
|
||||
authProvider.setEnabled(state.isEnabled());
|
||||
authProvider.setPriority(state.getPriority());
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
.defaultIfEmpty(providers)
|
||||
)
|
||||
.flatMap(providers -> fetchEnabledAuthProviders()
|
||||
.map(names -> providers.stream()
|
||||
.peek(provider -> {
|
||||
boolean enabled = names.contains(provider.getName());
|
||||
provider.setEnabled(enabled);
|
||||
})
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
.defaultIfEmpty(providers)
|
||||
);
|
||||
.sorted(Comparator.comparingInt(ListedAuthProvider::getPriority)
|
||||
.thenComparing(ListedAuthProvider::getName))
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AuthProvider> getEnabledProviders() {
|
||||
return fetchEnabledAuthProviders().flatMapMany(enabledNames -> {
|
||||
return fetchProviderStates().flatMapMany(states -> {
|
||||
var namePriorityMap = states.stream()
|
||||
// filter enabled providers
|
||||
.filter(SystemSetting.AuthProviderState::isEnabled)
|
||||
.collect(Collectors.toMap(SystemSetting.AuthProviderState::getName,
|
||||
SystemSetting.AuthProviderState::getPriority));
|
||||
|
||||
var listOptions = ListOptions.builder()
|
||||
.andQuery(QueryFactory.in("metadata.name", enabledNames))
|
||||
.andQuery(QueryFactory.in("metadata.name", namePriorityMap.keySet()))
|
||||
.andQuery(ExtensionUtil.notDeleting())
|
||||
.build();
|
||||
return client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort());
|
||||
return client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())
|
||||
.map(provider -> new AuthProviderWithPriority()
|
||||
.setAuthProvider(provider)
|
||||
.setPriority(namePriorityMap.getOrDefault(
|
||||
provider.getMetadata().getName(), 0)
|
||||
)
|
||||
)
|
||||
.sort(AuthProviderWithPriority::compareTo)
|
||||
.map(AuthProviderWithPriority::getAuthProvider);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Set<String>> fetchEnabledAuthProviders() {
|
||||
return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
|
||||
.map(configMap -> {
|
||||
SystemSetting.AuthProvider authProvider = getAuthProvider(configMap);
|
||||
return authProvider.getEnabled();
|
||||
});
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
static class AuthProviderWithPriority implements Comparable<AuthProviderWithPriority> {
|
||||
private AuthProvider authProvider;
|
||||
private int priority;
|
||||
|
||||
public String getName() {
|
||||
return authProvider.getMetadata().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull AuthProviderWithPriority o) {
|
||||
return Comparator.comparingInt(AuthProviderWithPriority::getPriority)
|
||||
.thenComparing(AuthProviderWithPriority::getName)
|
||||
.compare(this, o);
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<List<SystemSetting.AuthProviderState>> fetchProviderStates() {
|
||||
return fetchSystemConfigMap()
|
||||
.map(AuthProviderServiceImpl::getAuthProviderConfig)
|
||||
.map(SystemSetting.AuthProvider::getStates)
|
||||
.defaultIfEmpty(List.of())
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
Flux<UserConnection> listMyConnections() {
|
||||
|
@ -121,32 +166,6 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
|||
});
|
||||
}
|
||||
|
||||
private static Comparator<AuthProvider> defaultComparator() {
|
||||
return Comparator.comparing((AuthProvider item) -> item.getSpec().getPriority())
|
||||
.thenComparing(item -> item.getMetadata().getName())
|
||||
.thenComparing(item -> item.getMetadata().getCreationTimestamp());
|
||||
}
|
||||
|
||||
private Mono<ConfigMap> updateAuthProviderEnabled(Consumer<Set<String>> consumer) {
|
||||
return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
ConfigMap configMap = new ConfigMap();
|
||||
configMap.setMetadata(new Metadata());
|
||||
configMap.getMetadata().setName(SystemSetting.SYSTEM_CONFIG);
|
||||
configMap.setData(new HashMap<>());
|
||||
return client.create(configMap);
|
||||
}))
|
||||
.flatMap(configMap -> {
|
||||
SystemSetting.AuthProvider authProvider = getAuthProvider(configMap);
|
||||
consumer.accept(authProvider.getEnabled());
|
||||
|
||||
final Map<String, String> data = configMap.getData();
|
||||
data.put(SystemSetting.AuthProvider.GROUP,
|
||||
JsonUtils.objectToJson(authProvider));
|
||||
return client.update(configMap);
|
||||
});
|
||||
}
|
||||
|
||||
private ListedAuthProvider convertTo(AuthProvider authProvider) {
|
||||
return ListedAuthProvider.builder()
|
||||
.name(authProvider.getMetadata().getName())
|
||||
|
@ -159,6 +178,7 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
|||
.bindingUrl(authProvider.getSpec().getBindingUrl())
|
||||
.unbindingUrl(authProvider.getSpec().getUnbindUrl())
|
||||
.supportsBinding(supportsBinding(authProvider))
|
||||
.authType(authProvider.getSpec().getAuthType())
|
||||
.isBound(false)
|
||||
.enabled(false)
|
||||
.privileged(privileged(authProvider))
|
||||
|
@ -176,7 +196,7 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
private static SystemSetting.AuthProvider getAuthProvider(ConfigMap configMap) {
|
||||
private static SystemSetting.AuthProvider getAuthProviderConfig(ConfigMap configMap) {
|
||||
if (configMap.getData() == null) {
|
||||
configMap.setData(new HashMap<>());
|
||||
}
|
||||
|
@ -190,10 +210,41 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
|||
authProvider =
|
||||
JsonUtils.jsonToObject(providerGroup, SystemSetting.AuthProvider.class);
|
||||
}
|
||||
|
||||
if (authProvider.getEnabled() == null) {
|
||||
authProvider.setEnabled(new HashSet<>());
|
||||
if (authProvider.getStates() == null) {
|
||||
authProvider.setStates(new ArrayList<>());
|
||||
}
|
||||
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
private Mono<ConfigMap> updateAuthProviderEnabled(String name, boolean enabled) {
|
||||
return Mono.defer(() -> fetchSystemConfigMap()
|
||||
.flatMap(configMap -> {
|
||||
var providerConfig = getAuthProviderConfig(configMap);
|
||||
var stateToFoundOpt = providerConfig.getStates()
|
||||
.stream()
|
||||
.filter(state -> state.getName().equals(name))
|
||||
.findFirst();
|
||||
if (stateToFoundOpt.isEmpty()) {
|
||||
var state = new SystemSetting.AuthProviderState()
|
||||
.setName(name)
|
||||
.setEnabled(enabled);
|
||||
providerConfig.getStates().add(state);
|
||||
} else {
|
||||
stateToFoundOpt.get().setEnabled(enabled);
|
||||
}
|
||||
|
||||
configMap.getData().put(SystemSetting.AuthProvider.GROUP,
|
||||
JsonUtils.objectToJson(providerConfig));
|
||||
|
||||
return client.update(configMap);
|
||||
})
|
||||
)
|
||||
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||
}
|
||||
|
||||
Mono<ConfigMap> fetchSystemConfigMap() {
|
||||
return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import run.halo.app.core.extension.AuthProvider;
|
||||
|
||||
/**
|
||||
* A listed value object for {@link run.halo.app.core.extension.AuthProvider}.
|
||||
|
@ -35,11 +36,15 @@ public class ListedAuthProvider {
|
|||
|
||||
String unbindingUrl;
|
||||
|
||||
AuthProvider.AuthType authType;
|
||||
|
||||
Boolean isBound;
|
||||
|
||||
Boolean enabled;
|
||||
|
||||
int priority;
|
||||
|
||||
Boolean supportsBinding;
|
||||
|
||||
|
||||
Boolean privileged;
|
||||
}
|
||||
|
|
|
@ -61,14 +61,21 @@ class PreAuthLoginEndpoint {
|
|||
var publicKey = cryptoService.readPublicKey()
|
||||
.map(key -> Base64.getEncoder().encodeToString(key));
|
||||
var globalInfo = globalInfoService.getGlobalInfo().cache();
|
||||
var loginMethod = request.queryParam("method").orElse("local");
|
||||
var authProviders = authProviderService.getEnabledProviders().cache();
|
||||
var authProvider = authProviders
|
||||
.filter(ap -> Objects.equals(loginMethod, ap.getMetadata().getName()))
|
||||
.next()
|
||||
.switchIfEmpty(Mono.error(() -> new ServerWebInputException(
|
||||
"Invalid login method " + loginMethod)
|
||||
))
|
||||
|
||||
var allFormProviders = authProviders
|
||||
.filter(ap -> AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
|
||||
.cache();
|
||||
|
||||
var authProvider = Mono.justOrEmpty(request.queryParam("method"))
|
||||
.flatMap(method -> allFormProviders
|
||||
.filter(ap -> Objects.equals(method, ap.getMetadata().getName()))
|
||||
.next()
|
||||
.switchIfEmpty(Mono.error(
|
||||
() -> new ServerWebInputException("Invalid login method " + method))
|
||||
)
|
||||
)
|
||||
.switchIfEmpty(allFormProviders.next())
|
||||
.cache();
|
||||
|
||||
var fragmentTemplateName = authProvider.map(ap -> {
|
||||
|
@ -83,9 +90,12 @@ class PreAuthLoginEndpoint {
|
|||
var socialAuthProviders = authProviders
|
||||
.filter(ap -> !AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
|
||||
.cache();
|
||||
var formAuthProviders = authProviders
|
||||
.filter(ap -> AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
|
||||
.filter(ap -> !Objects.equals(loginMethod, ap.getMetadata().getName()))
|
||||
var formAuthProviders = allFormProviders
|
||||
.filterWhen(ap -> authProvider
|
||||
.map(provider -> !Objects.equals(provider.getMetadata().getName(),
|
||||
ap.getMetadata().getName())
|
||||
)
|
||||
)
|
||||
.cache();
|
||||
|
||||
return serverRequestCache.saveRequest(exchange).then(Mono.defer(() ->
|
||||
|
|
|
@ -9,7 +9,6 @@ import static org.mockito.Mockito.verify;
|
|||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -48,7 +47,7 @@ class AuthProviderServiceImplTest {
|
|||
AuthProviderServiceImpl authProviderService;
|
||||
|
||||
@Test
|
||||
void testEnable() {
|
||||
void testEnable() throws JSONException {
|
||||
// Create a test auth provider
|
||||
AuthProvider authProvider = createAuthProvider("github");
|
||||
when(client.get(eq(AuthProvider.class), eq("github"))).thenReturn(Mono.just(authProvider));
|
||||
|
@ -56,10 +55,7 @@ class AuthProviderServiceImplTest {
|
|||
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
||||
when(client.update(captor.capture())).thenReturn(Mono.empty());
|
||||
|
||||
ConfigMap configMap = new ConfigMap();
|
||||
configMap.setData(new HashMap<>());
|
||||
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
||||
.thenReturn(Mono.just(configMap));
|
||||
pileSystemConfigMap();
|
||||
|
||||
// Call the method being tested
|
||||
authProviderService.enable("github")
|
||||
|
@ -68,51 +64,59 @@ class AuthProviderServiceImplTest {
|
|||
.verifyComplete();
|
||||
|
||||
ConfigMap value = captor.getValue();
|
||||
String providerSettingStr = value.getData().get(SystemSetting.AuthProvider.GROUP);
|
||||
Set<String> enabled =
|
||||
JsonUtils.jsonToObject(providerSettingStr, SystemSetting.AuthProvider.class)
|
||||
.getEnabled();
|
||||
assertThat(enabled).containsExactly("github");
|
||||
JSONAssert.assertEquals("""
|
||||
{
|
||||
"states": [
|
||||
{
|
||||
"name": "github",
|
||||
"enabled": true,
|
||||
"priority": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
""",
|
||||
value.getData().get(SystemSetting.AuthProvider.GROUP),
|
||||
true);
|
||||
// Verify the result
|
||||
verify(client).get(AuthProvider.class, "github");
|
||||
verify(client).fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDisable() {
|
||||
void testDisable() throws JSONException {
|
||||
// Create a test auth provider
|
||||
AuthProvider authProvider = createAuthProvider("github");
|
||||
when(client.get(eq(AuthProvider.class), eq("github"))).thenReturn(Mono.just(authProvider));
|
||||
|
||||
AuthProvider local = createAuthProvider("local");
|
||||
local.getMetadata().getLabels().put(AuthProvider.PRIVILEGED_LABEL, "true");
|
||||
// when(client.list(eq(AuthProvider.class), any(), any())).thenReturn(Flux.just(local));
|
||||
|
||||
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
||||
when(client.update(captor.capture())).thenReturn(Mono.empty());
|
||||
|
||||
ConfigMap configMap = new ConfigMap();
|
||||
configMap.setData(new HashMap<>());
|
||||
configMap.getData().put(SystemSetting.AuthProvider.GROUP, "{\"enabled\":[\"github\"]}");
|
||||
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
||||
.thenReturn(Mono.just(configMap));
|
||||
pileSystemConfigMap();
|
||||
|
||||
// Call the method being tested
|
||||
Mono<AuthProvider> result = authProviderService.disable("github");
|
||||
|
||||
assertEquals(authProvider, result.block());
|
||||
ConfigMap value = captor.getValue();
|
||||
String providerSettingStr = value.getData().get(SystemSetting.AuthProvider.GROUP);
|
||||
Set<String> enabled =
|
||||
JsonUtils.jsonToObject(providerSettingStr, SystemSetting.AuthProvider.class)
|
||||
.getEnabled();
|
||||
assertThat(enabled).isEmpty();
|
||||
JSONAssert.assertEquals("""
|
||||
{
|
||||
"states": [
|
||||
{
|
||||
"name": "github",
|
||||
"enabled": false,
|
||||
"priority": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
""",
|
||||
value.getData().get(SystemSetting.AuthProvider.GROUP),
|
||||
true);
|
||||
// Verify the result
|
||||
verify(client).get(AuthProvider.class, "github");
|
||||
verify(client).fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void listAll() {
|
||||
|
@ -129,11 +133,7 @@ class AuthProviderServiceImplTest {
|
|||
when(client.listAll(same(UserConnection.class), any(ListOptions.class), any(Sort.class)))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
ConfigMap configMap = new ConfigMap();
|
||||
configMap.setData(new HashMap<>());
|
||||
configMap.getData().put(SystemSetting.AuthProvider.GROUP, "{\"enabled\":[\"github\"]}");
|
||||
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
||||
.thenReturn(Mono.just(configMap));
|
||||
pileSystemConfigMap();
|
||||
|
||||
authProviderService.listAll()
|
||||
.as(StepVerifier::create)
|
||||
|
@ -142,29 +142,36 @@ class AuthProviderServiceImplTest {
|
|||
try {
|
||||
JSONAssert.assertEquals("""
|
||||
[{
|
||||
"name": "github",
|
||||
"displayName": "github",
|
||||
"bindingUrl": "fake-binding-url",
|
||||
"enabled": true,
|
||||
"isBound": false,
|
||||
"supportsBinding": false,
|
||||
"privileged": false
|
||||
}, {
|
||||
"name": "gitlab",
|
||||
"displayName": "gitlab",
|
||||
"bindingUrl": "fake-binding-url",
|
||||
"enabled": false,
|
||||
"isBound": false,
|
||||
"supportsBinding": false,
|
||||
"privileged": false
|
||||
},{
|
||||
|
||||
"name": "gitee",
|
||||
"displayName": "gitee",
|
||||
"enabled": false,
|
||||
"isBound": false,
|
||||
"supportsBinding": false,
|
||||
"privileged": false
|
||||
"name": "gitee",
|
||||
"displayName": "gitee",
|
||||
"authType": "OAUTH2",
|
||||
"isBound": false,
|
||||
"enabled": false,
|
||||
"priority": 0,
|
||||
"supportsBinding": false,
|
||||
"privileged": false
|
||||
},
|
||||
{
|
||||
"name": "github",
|
||||
"displayName": "github",
|
||||
"bindingUrl": "fake-binding-url",
|
||||
"authType": "OAUTH2",
|
||||
"isBound": false,
|
||||
"enabled": false,
|
||||
"priority": 0,
|
||||
"supportsBinding": false,
|
||||
"privileged": false
|
||||
},
|
||||
{
|
||||
"name": "gitlab",
|
||||
"displayName": "gitlab",
|
||||
"bindingUrl": "fake-binding-url",
|
||||
"authType": "OAUTH2",
|
||||
"isBound": false,
|
||||
"enabled": false,
|
||||
"priority": 0,
|
||||
"supportsBinding": false,
|
||||
"privileged": false
|
||||
}]
|
||||
""",
|
||||
JsonUtils.objectToJson(result),
|
||||
|
@ -183,6 +190,14 @@ class AuthProviderServiceImplTest {
|
|||
authProvider.getMetadata().setLabels(new HashMap<>());
|
||||
authProvider.setSpec(new AuthProvider.AuthProviderSpec());
|
||||
authProvider.getSpec().setDisplayName(name);
|
||||
authProvider.getSpec().setAuthType(AuthProvider.AuthType.OAUTH2);
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
void pileSystemConfigMap() {
|
||||
ConfigMap configMap = new ConfigMap();
|
||||
configMap.setData(new HashMap<>());
|
||||
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
||||
.thenReturn(Mono.just(configMap));
|
||||
}
|
||||
}
|
|
@ -1,19 +1,22 @@
|
|||
<script lang="ts" setup>
|
||||
import type { AuthProvider, ListedAuthProvider } from "@halo-dev/api-client";
|
||||
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
|
||||
import type { ListedAuthProvider } from "@halo-dev/api-client";
|
||||
import {
|
||||
AuthProviderSpecAuthTypeEnum,
|
||||
consoleApiClient,
|
||||
} from "@halo-dev/api-client";
|
||||
import {
|
||||
IconLockPasswordLine,
|
||||
VCard,
|
||||
VLoading,
|
||||
VPageHeader,
|
||||
} from "@halo-dev/components";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import Fuse from "fuse.js";
|
||||
import { computed, ref } from "vue";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import AuthProviderListItem from "./components/AuthProviderListItem.vue";
|
||||
import { ref } from "vue";
|
||||
import AuthProvidersSection from "./components/AuthProvidersSection.vue";
|
||||
|
||||
const authProviders = ref<ListedAuthProvider[]>([]);
|
||||
const SYSTEM_CONFIGMAP_AUTH_PROVIDER = "authProvider";
|
||||
|
||||
const formAuthProviders = ref<ListedAuthProvider[]>([]);
|
||||
const oauth2AuthProviders = ref<ListedAuthProvider[]>([]);
|
||||
|
||||
const { isLoading, refetch } = useQuery<ListedAuthProvider[]>({
|
||||
queryKey: ["auth-providers"],
|
||||
|
@ -23,26 +26,17 @@ const { isLoading, refetch } = useQuery<ListedAuthProvider[]>({
|
|||
return data;
|
||||
},
|
||||
onSuccess(data) {
|
||||
authProviders.value = data;
|
||||
fuse = new Fuse(data, {
|
||||
keys: ["name", "displayName"],
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.2,
|
||||
});
|
||||
formAuthProviders.value = data.filter(
|
||||
(authProvider) =>
|
||||
authProvider.authType === AuthProviderSpecAuthTypeEnum.Form
|
||||
);
|
||||
oauth2AuthProviders.value = data.filter(
|
||||
(authProvider) =>
|
||||
authProvider.authType === AuthProviderSpecAuthTypeEnum.Oauth2
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const keyword = ref("");
|
||||
let fuse: Fuse<ListedAuthProvider> | undefined = undefined;
|
||||
|
||||
const searchResults = computed(() => {
|
||||
if (!fuse || !keyword.value) {
|
||||
return authProviders.value;
|
||||
}
|
||||
|
||||
return fuse?.search(keyword.value).map((item) => item.item);
|
||||
});
|
||||
|
||||
// Drag and drop
|
||||
const updating = ref(false);
|
||||
|
||||
|
@ -50,29 +44,23 @@ async function onSortUpdate() {
|
|||
try {
|
||||
updating.value = true;
|
||||
|
||||
const { data: rawAuthProviders } =
|
||||
await coreApiClient.auth.authProvider.listAuthProvider();
|
||||
const allAuthProviders = [
|
||||
...formAuthProviders.value,
|
||||
...oauth2AuthProviders.value,
|
||||
].filter(Boolean);
|
||||
|
||||
const authProviderNames = authProviders.value.map((item) => item.name);
|
||||
|
||||
const sortedAuthProviders = authProviderNames
|
||||
.map((name) => {
|
||||
const authProvider = rawAuthProviders.items.find(
|
||||
(item) => item.metadata.name === name
|
||||
);
|
||||
if (authProvider) {
|
||||
authProvider.spec.priority = authProviderNames.indexOf(name);
|
||||
}
|
||||
return authProvider;
|
||||
})
|
||||
.filter(Boolean) as AuthProvider[];
|
||||
|
||||
for (const authProvider of sortedAuthProviders) {
|
||||
await coreApiClient.auth.authProvider.updateAuthProvider({
|
||||
name: authProvider.metadata.name,
|
||||
authProvider: authProvider,
|
||||
});
|
||||
}
|
||||
await consoleApiClient.configMap.system.updateSystemConfigByGroup({
|
||||
group: SYSTEM_CONFIGMAP_AUTH_PROVIDER,
|
||||
body: {
|
||||
states: allAuthProviders.map((authProvider, index) => {
|
||||
return {
|
||||
name: authProvider.name,
|
||||
enabled: authProvider.enabled,
|
||||
priority: index,
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await refetch();
|
||||
updating.value = false;
|
||||
|
@ -87,46 +75,25 @@ async function onSortUpdate() {
|
|||
</template>
|
||||
</VPageHeader>
|
||||
|
||||
<div class="m-0 md:m-4">
|
||||
<VCard :body-class="['!p-0']">
|
||||
<template #header>
|
||||
<div class="block w-full bg-gray-50 px-4 py-3">
|
||||
<div
|
||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="flex w-full flex-1 sm:w-auto">
|
||||
<FormKit
|
||||
v-model="keyword"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
></FormKit>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else appear name="fade">
|
||||
<VueDraggable
|
||||
v-model="authProviders"
|
||||
ghost-class="opacity-50"
|
||||
handle=".drag-element"
|
||||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
:class="{
|
||||
'cursor-progress opacity-60': updating,
|
||||
}"
|
||||
role="list"
|
||||
tag="ul"
|
||||
:disabled="updating"
|
||||
@update="onSortUpdate"
|
||||
>
|
||||
<li v-for="(authProvider, index) in searchResults" :key="index">
|
||||
<AuthProviderListItem
|
||||
:auth-provider="authProvider"
|
||||
@reload="refetch"
|
||||
/>
|
||||
</li>
|
||||
</VueDraggable>
|
||||
</Transition>
|
||||
</VCard>
|
||||
<div class="m-0 space-y-5 md:m-4">
|
||||
<VLoading v-if="isLoading" />
|
||||
<TransitionGroup v-else appear name="fade">
|
||||
<AuthProvidersSection
|
||||
:key="AuthProviderSpecAuthTypeEnum.Form"
|
||||
v-model="formAuthProviders"
|
||||
:title="$t('core.identity_authentication.list.types.form')"
|
||||
:loading="updating"
|
||||
@update="onSortUpdate"
|
||||
/>
|
||||
|
||||
<AuthProvidersSection
|
||||
v-if="oauth2AuthProviders.length"
|
||||
:key="AuthProviderSpecAuthTypeEnum.Oauth2"
|
||||
v-model="oauth2AuthProviders"
|
||||
:title="$t('core.identity_authentication.list.types.oauth2')"
|
||||
:loading="updating"
|
||||
@update="onSortUpdate"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<script setup lang="ts">
|
||||
import type { ListedAuthProvider } from "@halo-dev/api-client";
|
||||
import { VCard } from "@halo-dev/components";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import AuthProviderListItem from "./AuthProviderListItem.vue";
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const modelValue = defineModel<ListedAuthProvider[]>({ default: [] });
|
||||
|
||||
const { loading = false } = defineProps<{
|
||||
loading?: boolean;
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update"): void;
|
||||
}>();
|
||||
|
||||
function onReload() {
|
||||
queryClient.invalidateQueries({ queryKey: ["auth-providers"] });
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VCard :title="title" :body-class="['!p-0']">
|
||||
<VueDraggable
|
||||
v-model="modelValue"
|
||||
ghost-class="opacity-50"
|
||||
handle=".drag-element"
|
||||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
:class="{
|
||||
'cursor-progress opacity-60': loading,
|
||||
}"
|
||||
role="list"
|
||||
tag="ul"
|
||||
:disabled="loading"
|
||||
@update="emit('update')"
|
||||
>
|
||||
<li v-for="authProvider in modelValue" :key="authProvider.name">
|
||||
<AuthProviderListItem
|
||||
:auth-provider="authProvider"
|
||||
@reload="onReload"
|
||||
/>
|
||||
</li>
|
||||
</VueDraggable>
|
||||
</VCard>
|
||||
</template>
|
|
@ -16,6 +16,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "core.identity_authentication.title",
|
||||
searchable: true,
|
||||
permissions: ["*"],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -24,6 +25,7 @@ export default definePlugin({
|
|||
component: AuthProviderDetail,
|
||||
meta: {
|
||||
title: "core.identity_authentication.detail.title",
|
||||
permissions: ["*"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -292,12 +292,14 @@ function onGrantPermissionModalClose() {
|
|||
</template>
|
||||
{{ $t("core.user.actions.roles") }}
|
||||
</VButton>
|
||||
<VButton :route="{ name: 'AuthProviders' }" size="sm" type="default">
|
||||
<template #icon>
|
||||
<IconLockPasswordLine class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.user.actions.identity_authentication") }}
|
||||
</VButton>
|
||||
<HasPermission :permissions="['*']">
|
||||
<VButton :route="{ name: 'AuthProviders' }" size="sm" type="default">
|
||||
<template #icon>
|
||||
<IconLockPasswordLine class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.user.actions.identity_authentication") }}
|
||||
</VButton>
|
||||
</HasPermission>
|
||||
<VButton
|
||||
v-permission="['system:users:manage']"
|
||||
type="secondary"
|
||||
|
|
|
@ -31,7 +31,7 @@ export interface AuthProviderSpec {
|
|||
* @type {string}
|
||||
* @memberof AuthProviderSpec
|
||||
*/
|
||||
'authType'?: AuthProviderSpecAuthTypeEnum;
|
||||
'authType': AuthProviderSpecAuthTypeEnum;
|
||||
/**
|
||||
* Authentication url of the auth provider
|
||||
* @type {string}
|
||||
|
@ -80,12 +80,6 @@ export interface AuthProviderSpec {
|
|||
* @memberof AuthProviderSpec
|
||||
*/
|
||||
'method'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AuthProviderSpec
|
||||
*/
|
||||
'priority'?: number;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
|
|
|
@ -20,6 +20,12 @@
|
|||
* @interface ListedAuthProvider
|
||||
*/
|
||||
export interface ListedAuthProvider {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ListedAuthProvider
|
||||
*/
|
||||
'authType'?: ListedAuthProviderAuthTypeEnum;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -74,6 +80,12 @@ export interface ListedAuthProvider {
|
|||
* @memberof ListedAuthProvider
|
||||
*/
|
||||
'name': string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ListedAuthProvider
|
||||
*/
|
||||
'priority'?: number;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
|
@ -100,3 +112,11 @@ export interface ListedAuthProvider {
|
|||
'website'?: string;
|
||||
}
|
||||
|
||||
export const ListedAuthProviderAuthTypeEnum = {
|
||||
Form: 'FORM',
|
||||
Oauth2: 'OAUTH2'
|
||||
} as const;
|
||||
|
||||
export type ListedAuthProviderAuthTypeEnum = typeof ListedAuthProviderAuthTypeEnum[keyof typeof ListedAuthProviderAuthTypeEnum];
|
||||
|
||||
|
||||
|
|
|
@ -1140,6 +1140,10 @@ core:
|
|||
local: Login with credentials
|
||||
description:
|
||||
local: Default login method built into Halo
|
||||
list:
|
||||
types:
|
||||
form: Basic Authentication Method
|
||||
oauth2: Third-party Authentication Method
|
||||
detail:
|
||||
title: Identity authentication detail
|
||||
fields:
|
||||
|
|
|
@ -1068,6 +1068,10 @@ core:
|
|||
local: 账号密码登录
|
||||
description:
|
||||
local: Halo 内置的默认登录方式
|
||||
list:
|
||||
types:
|
||||
form: 基础认证方式
|
||||
oauth2: 三方认证方式
|
||||
detail:
|
||||
title: 身份认证详情
|
||||
fields:
|
||||
|
|
|
@ -1045,6 +1045,10 @@ core:
|
|||
local: 帳號密碼登入
|
||||
description:
|
||||
local: Halo 內建的默認登入方式
|
||||
list:
|
||||
types:
|
||||
form: 基礎認證方式
|
||||
oauth2: 三方認證方式
|
||||
detail:
|
||||
title: 身份認證詳情
|
||||
fields:
|
||||
|
|
Loading…
Reference in New Issue