From 8e17e5468c2d1d7dedb65e39439f01a44223cc49 Mon Sep 17 00:00:00 2001 From: Ahoo Wang Date: Wed, 14 Jul 2021 23:23:45 +0800 Subject: [PATCH 1/4] add RBAC module --- build.gradle.kts | 2 + .../ahoo/cosky/config/ConfigKeyGenerator.java | 6 +- .../cosky/core/{Consts.java => CoSky.java} | 2 +- .../java/me/ahoo/cosky/core/Namespaced.java | 4 +- .../me/ahoo/cosky/core/NamespacedContext.java | 2 +- .../cosky/core/redis/RedisConnection.java | 13 +- .../core/redis/RedisConnectionFactory.java | 13 +- .../api/authenticate/AuthenticateClient.ts | 37 +++++ .../src/app/api/authenticate/LoginResponse.ts | 18 ++ .../src/app/api/authenticate/Token.ts | 17 ++ .../src/app/api/authenticate/TokenPayload.ts | 20 +++ .../src/app/api/role/ResourceActionDto.ts | 17 ++ .../src/app/api/role/RoleClient.ts | 40 +++++ .../src/app/api/user/UserClient.ts | 51 ++++++ cosky-dashboard/src/app/api/user/UserDto.ts | 17 ++ cosky-dashboard/src/app/app-routing.module.ts | 16 +- cosky-dashboard/src/app/app.component.html | 13 +- cosky-dashboard/src/app/app.module.ts | 17 +- .../app/components/login/login.component.html | 20 +++ .../app/components/login/login.component.scss | 4 + .../components/login/login.component.spec.ts | 25 +++ .../app/components/login/login.component.ts | 31 ++++ .../app/components/role/role.component.html | 38 +++++ .../app/components/role/role.component.scss | 0 .../components/role/role.component.spec.ts | 25 +++ .../src/app/components/role/role.component.ts | 39 +++++ .../app/components/user/user.component.html | 39 +++++ .../app/components/user/user.component.scss | 0 .../components/user/user.component.spec.ts | 25 +++ .../src/app/components/user/user.component.ts | 39 +++++ cosky-dashboard/src/app/security/AuthGuard.ts | 51 ++++++ .../src/app/security/AuthInterceptor.ts | 41 +++++ .../src/app/security/SecurityService.ts | 128 ++++++++++++++ cosky-dashboard/src/index.html | 3 +- cosky-dependencies/build.gradle.kts | 2 +- .../discovery/DiscoveryKeyGenerator.java | 4 +- .../discovery/DiscoveryKeyGeneratorTest.java | 8 +- cosky-rest-api/build.gradle.kts | 9 +- .../me/ahoo/cosky/rest/config/AppConfig.java | 3 + .../cosky/rest/config/DashboardConfig.java | 4 +- .../ahoo/cosky/rest/config/SwaggerConfig.java | 73 ++++++++ .../controller/AuthenticateController.java | 49 ++++++ .../rest/controller/ConfigController.java | 4 +- .../rest/controller/NamespaceController.java | 6 +- .../cosky/rest/controller/RoleController.java | 53 ++++++ .../cosky/rest/controller/UserController.java | 67 ++++++++ .../role/ResourceActionDto.java} | 33 ++-- .../user/AddUserRequest.java} | 27 ++- .../user/ChangePwdRequest.java} | 36 ++-- .../user/LoginRequest.java} | 30 ++-- ...horizeResponse.java => LoginResponse.java} | 3 +- .../cosky/rest/dto/user/RefreshRequest.java | 20 +++ .../java/me/ahoo/cosky/rest/rbac/Role.java | 132 --------------- .../security/AuthorizeHandlerInterceptor.java | 77 +++++++++ .../ConditionalOnSecurityEnabled.java | 31 ++++ .../ahoo/cosky/rest/security/JwtProvider.java | 132 +++++++++++++++ .../cosky/rest/security/SecurityCommand.java | 43 +++++ .../cosky/rest/security/SecurityContext.java | 32 ++++ .../rest/security/SecurityException.java | 94 +++++++++++ .../rest/security/SecurityProperties.java | 98 +++++++++++ .../rest/security/config/SecurityConfig.java | 47 ++++++ .../config/SecurityInterceptorConfigurer.java | 55 ++++++ .../ahoo/cosky/rest/security/rbac/Action.java | 82 +++++++++ .../security/rbac/NotFoundRoleException.java | 32 ++++ .../cosky/rest/security/rbac/RBACService.java | 137 +++++++++++++++ .../rest/security/rbac/RequestAction.java | 37 +++++ .../rest/security/rbac/ResourceAction.java | 67 ++++++++ .../ahoo/cosky/rest/security/rbac/Role.java | 76 +++++++++ .../rbac/annotation/AdminResource.java | 27 +++ .../ahoo/cosky/rest/security/user/User.java | 57 +++++++ .../cosky/rest/security/user/UserService.java | 156 ++++++++++++++++++ .../cosky/rest/support/RequestPathPrefix.java | 20 +++ .../src/main/resources/application.yaml | 18 +- .../cosky/spring/cloud/CoskyProperties.java | 4 +- .../cloud/CoskyPropertySourceLocator.java | 4 +- 75 files changed, 2456 insertions(+), 246 deletions(-) rename cosky-core/src/main/java/me/ahoo/cosky/core/{Consts.java => CoSky.java} (96%) create mode 100644 cosky-dashboard/src/app/api/authenticate/AuthenticateClient.ts create mode 100644 cosky-dashboard/src/app/api/authenticate/LoginResponse.ts create mode 100644 cosky-dashboard/src/app/api/authenticate/Token.ts create mode 100644 cosky-dashboard/src/app/api/authenticate/TokenPayload.ts create mode 100644 cosky-dashboard/src/app/api/role/ResourceActionDto.ts create mode 100644 cosky-dashboard/src/app/api/role/RoleClient.ts create mode 100644 cosky-dashboard/src/app/api/user/UserClient.ts create mode 100644 cosky-dashboard/src/app/api/user/UserDto.ts create mode 100644 cosky-dashboard/src/app/components/login/login.component.html create mode 100644 cosky-dashboard/src/app/components/login/login.component.scss create mode 100644 cosky-dashboard/src/app/components/login/login.component.spec.ts create mode 100644 cosky-dashboard/src/app/components/login/login.component.ts create mode 100644 cosky-dashboard/src/app/components/role/role.component.html create mode 100644 cosky-dashboard/src/app/components/role/role.component.scss create mode 100644 cosky-dashboard/src/app/components/role/role.component.spec.ts create mode 100644 cosky-dashboard/src/app/components/role/role.component.ts create mode 100644 cosky-dashboard/src/app/components/user/user.component.html create mode 100644 cosky-dashboard/src/app/components/user/user.component.scss create mode 100644 cosky-dashboard/src/app/components/user/user.component.spec.ts create mode 100644 cosky-dashboard/src/app/components/user/user.component.ts create mode 100644 cosky-dashboard/src/app/security/AuthGuard.ts create mode 100644 cosky-dashboard/src/app/security/AuthInterceptor.ts create mode 100644 cosky-dashboard/src/app/security/SecurityService.ts create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/SwaggerConfig.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/AuthenticateController.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/RoleController.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/UserController.java rename cosky-rest-api/src/main/java/me/ahoo/cosky/rest/{user/User.java => dto/role/ResourceActionDto.java} (59%) rename cosky-rest-api/src/main/java/me/ahoo/cosky/rest/{rbac/UserRoleBinding.java => dto/user/AddUserRequest.java} (61%) rename cosky-rest-api/src/main/java/me/ahoo/cosky/rest/{rbac/RBACService.java => dto/user/ChangePwdRequest.java} (59%) rename cosky-rest-api/src/main/java/me/ahoo/cosky/rest/{user/UserService.java => dto/user/LoginRequest.java} (61%) rename cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/{AuthorizeResponse.java => LoginResponse.java} (97%) create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/RefreshRequest.java delete mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/Role.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/AuthorizeHandlerInterceptor.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/ConditionalOnSecurityEnabled.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/JwtProvider.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityCommand.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityContext.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityException.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityProperties.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/config/SecurityConfig.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/config/SecurityInterceptorConfigurer.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/Action.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/NotFoundRoleException.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RBACService.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RequestAction.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/ResourceAction.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/Role.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/annotation/AdminResource.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/User.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/UserService.java diff --git a/build.gradle.kts b/build.gradle.kts index 9f855188..35da645d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,8 @@ ext { set("commonsIOVersion", "2.10.0") set("springfoxVersion", "3.0.0") set("metricsVersion", "4.2.0") + set("jjwtVersion", "0.11.2") + set("cosIdVersion", "1.3.1") set("libraryProjects", libraryProjects) } diff --git a/cosky-config/src/main/java/me/ahoo/cosky/config/ConfigKeyGenerator.java b/cosky-config/src/main/java/me/ahoo/cosky/config/ConfigKeyGenerator.java index c6ecca9c..4cd38548 100644 --- a/cosky-config/src/main/java/me/ahoo/cosky/config/ConfigKeyGenerator.java +++ b/cosky-config/src/main/java/me/ahoo/cosky/config/ConfigKeyGenerator.java @@ -15,7 +15,7 @@ import com.google.common.base.Strings; import lombok.var; -import me.ahoo.cosky.core.Consts; +import me.ahoo.cosky.core.CoSky; /** * @author ahoo wang @@ -80,7 +80,7 @@ public static String getConfigKey(String namespace, String configId) { } public static NamespacedConfigId getConfigIdOfKey(String configKey) { - var firstSplitIdx = configKey.indexOf(Consts.KEY_SEPARATOR); + var firstSplitIdx = configKey.indexOf(CoSky.KEY_SEPARATOR); var namespace = configKey.substring(0, firstSplitIdx); var configKeyPrefix = Strings.lenientFormat(configKeyPrefixFormat, namespace); var configId = configKey.substring(configKeyPrefix.length()); @@ -90,7 +90,7 @@ public static NamespacedConfigId getConfigIdOfKey(String configKey) { public static ConfigVersion getConfigVersionOfHistoryKey(String namespace, String configHistoryKey) { var configHistoryKeyPrefix = Strings.lenientFormat(configHistoryKeyPrefixFormat, namespace); var configIdWithVersion = configHistoryKey.substring(configHistoryKeyPrefix.length()); - var configIdWithVersionSplit = configIdWithVersion.split(Consts.KEY_SEPARATOR); + var configIdWithVersionSplit = configIdWithVersion.split(CoSky.KEY_SEPARATOR); if (configIdWithVersionSplit.length != 2) { throw new IllegalArgumentException(Strings.lenientFormat("configHistoryKey:[%s] format error.", configHistoryKey)); } diff --git a/cosky-core/src/main/java/me/ahoo/cosky/core/Consts.java b/cosky-core/src/main/java/me/ahoo/cosky/core/CoSky.java similarity index 96% rename from cosky-core/src/main/java/me/ahoo/cosky/core/Consts.java rename to cosky-core/src/main/java/me/ahoo/cosky/core/CoSky.java index 99445c62..8789a71b 100644 --- a/cosky-core/src/main/java/me/ahoo/cosky/core/Consts.java +++ b/cosky-core/src/main/java/me/ahoo/cosky/core/CoSky.java @@ -16,7 +16,7 @@ /** * @author ahoo wang */ -public interface Consts { +public interface CoSky { String COSKY = "cosky"; String KEY_SEPARATOR = ":"; } diff --git a/cosky-core/src/main/java/me/ahoo/cosky/core/Namespaced.java b/cosky-core/src/main/java/me/ahoo/cosky/core/Namespaced.java index fc0b0394..a12620a0 100644 --- a/cosky-core/src/main/java/me/ahoo/cosky/core/Namespaced.java +++ b/cosky-core/src/main/java/me/ahoo/cosky/core/Namespaced.java @@ -17,8 +17,8 @@ * @author ahoo wang */ public interface Namespaced { - String DEFAULT = Consts.COSKY + "-{default}"; - String SYSTEM = Consts.COSKY + "-{system}"; + String DEFAULT = CoSky.COSKY + "-{default}"; + String SYSTEM = CoSky.COSKY + "-{system}"; /** * 获取当前上下文的命名空间 diff --git a/cosky-core/src/main/java/me/ahoo/cosky/core/NamespacedContext.java b/cosky-core/src/main/java/me/ahoo/cosky/core/NamespacedContext.java index 5ff86190..0076e2ea 100644 --- a/cosky-core/src/main/java/me/ahoo/cosky/core/NamespacedContext.java +++ b/cosky-core/src/main/java/me/ahoo/cosky/core/NamespacedContext.java @@ -21,7 +21,7 @@ public interface NamespacedContext extends Namespaced { /** * 全局命名空间上下文 */ - NamespacedContext GLOBAL = of(Consts.COSKY); + NamespacedContext GLOBAL = of(CoSky.COSKY); /** * 设置当前上下文的命名空间 diff --git a/cosky-core/src/main/java/me/ahoo/cosky/core/redis/RedisConnection.java b/cosky-core/src/main/java/me/ahoo/cosky/core/redis/RedisConnection.java index a268798b..0aac70be 100644 --- a/cosky-core/src/main/java/me/ahoo/cosky/core/redis/RedisConnection.java +++ b/cosky-core/src/main/java/me/ahoo/cosky/core/redis/RedisConnection.java @@ -15,6 +15,7 @@ import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; +import io.lettuce.core.cluster.api.sync.RedisClusterCommands; /** @@ -22,10 +23,16 @@ */ public class RedisConnection implements AutoCloseable { private StatefulConnection connection; + private RedisClusterCommands syncCommands; private RedisClusterAsyncCommands asyncCommands; - public RedisConnection(StatefulConnection connection, RedisClusterAsyncCommands asyncCommands) { + public RedisConnection(StatefulConnection connection + , RedisClusterCommands syncCommands + , RedisClusterAsyncCommands asyncCommands + + ) { this.connection = connection; + this.syncCommands = syncCommands; this.asyncCommands = asyncCommands; } @@ -33,6 +40,10 @@ public StatefulConnection getConnection() { return connection; } + public RedisClusterCommands getSyncCommands() { + return syncCommands; + } + public RedisClusterAsyncCommands getAsyncCommands() { return asyncCommands; } diff --git a/cosky-core/src/main/java/me/ahoo/cosky/core/redis/RedisConnectionFactory.java b/cosky-core/src/main/java/me/ahoo/cosky/core/redis/RedisConnectionFactory.java index d6873979..44fa9bf9 100644 --- a/cosky-core/src/main/java/me/ahoo/cosky/core/redis/RedisConnectionFactory.java +++ b/cosky-core/src/main/java/me/ahoo/cosky/core/redis/RedisConnectionFactory.java @@ -19,6 +19,7 @@ import io.lettuce.core.RedisURI; import io.lettuce.core.cluster.RedisClusterClient; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; +import io.lettuce.core.cluster.api.sync.RedisClusterCommands; import io.lettuce.core.codec.StringCodec; import io.lettuce.core.masterreplica.MasterReplica; import io.lettuce.core.masterreplica.StatefulRedisMasterReplicaConnection; @@ -68,29 +69,33 @@ public synchronized RedisConnection getShareConnection() { return shareConnection; } - public synchronized RedisClusterAsyncCommands getShareAsyncCommands() { + public RedisClusterAsyncCommands getShareAsyncCommands() { return getShareConnection().getAsyncCommands(); } + public RedisClusterCommands getShareSyncCommands() { + return getShareConnection().getSyncCommands(); + } + public RedisConnection getConnection() { if (client instanceof RedisClusterClient) { var clusterConnection = ((RedisClusterClient) client).connect(); - return new RedisConnection(clusterConnection, clusterConnection.async()); + return new RedisConnection(clusterConnection, clusterConnection.sync(), clusterConnection.async()); } var redisClient = (RedisClient) client; if (Objects.isNull(redisConfig.getReadFrom())) { var connection = redisClient.connect(); - return new RedisConnection(connection, connection.async()); + return new RedisConnection(connection, connection.sync(), connection.async()); } ReadFrom readFrom = ReadFrom.valueOf(redisConfig.getReadFrom().name()); StatefulRedisMasterReplicaConnection connection = MasterReplica.connect(redisClient, StringCodec.UTF8, RedisURI.create(redisConfig.getUrl())); connection.setReadFrom(readFrom); - return new RedisConnection(connection, connection.async()); + return new RedisConnection(connection, connection.sync(), connection.async()); } public MessageListenable getMessageListenable() { diff --git a/cosky-dashboard/src/app/api/authenticate/AuthenticateClient.ts b/cosky-dashboard/src/app/api/authenticate/AuthenticateClient.ts new file mode 100644 index 00000000..550a5010 --- /dev/null +++ b/cosky-dashboard/src/app/api/authenticate/AuthenticateClient.ts @@ -0,0 +1,37 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Injectable} from "@angular/core"; +import {environment} from "../../../environments/environment"; +import {Observable} from "rxjs"; +import {HttpClient} from "@angular/common/http"; +import {LoginResponse} from "./LoginResponse"; + + +@Injectable({providedIn: 'root'}) +export class AuthenticateClient { + apiPrefix = environment.coskyRestApiHost + '/authenticate'; + + constructor(private httpClient: HttpClient) { + + } + + login(username: string, password: string): Observable { + const apiUrl = `${this.apiPrefix}/login`; + return this.httpClient.post(apiUrl, {username, password}); + } + + refresh(accessToken: string, refreshToken: string): Observable { + const apiUrl = `${this.apiPrefix}/refresh`; + return this.httpClient.post(apiUrl, {accessToken, refreshToken}); + } +} diff --git a/cosky-dashboard/src/app/api/authenticate/LoginResponse.ts b/cosky-dashboard/src/app/api/authenticate/LoginResponse.ts new file mode 100644 index 00000000..1801e4a0 --- /dev/null +++ b/cosky-dashboard/src/app/api/authenticate/LoginResponse.ts @@ -0,0 +1,18 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Token} from "./Token"; + +export interface LoginResponse extends Token{ + +} diff --git a/cosky-dashboard/src/app/api/authenticate/Token.ts b/cosky-dashboard/src/app/api/authenticate/Token.ts new file mode 100644 index 00000000..89dfbfde --- /dev/null +++ b/cosky-dashboard/src/app/api/authenticate/Token.ts @@ -0,0 +1,17 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Token { + accessToken: string; + refreshToken: string; +} diff --git a/cosky-dashboard/src/app/api/authenticate/TokenPayload.ts b/cosky-dashboard/src/app/api/authenticate/TokenPayload.ts new file mode 100644 index 00000000..d8a58632 --- /dev/null +++ b/cosky-dashboard/src/app/api/authenticate/TokenPayload.ts @@ -0,0 +1,20 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface TokenPayload { + jti: string; + sub: string; + role: string; + iat: number; + exp: number; +} diff --git a/cosky-dashboard/src/app/api/role/ResourceActionDto.ts b/cosky-dashboard/src/app/api/role/ResourceActionDto.ts new file mode 100644 index 00000000..a016632c --- /dev/null +++ b/cosky-dashboard/src/app/api/role/ResourceActionDto.ts @@ -0,0 +1,17 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface ResourceActionDto { + namespace: string; + action: 'r' | 'w' | 'rw'; +} diff --git a/cosky-dashboard/src/app/api/role/RoleClient.ts b/cosky-dashboard/src/app/api/role/RoleClient.ts new file mode 100644 index 00000000..23b78f0e --- /dev/null +++ b/cosky-dashboard/src/app/api/role/RoleClient.ts @@ -0,0 +1,40 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Injectable} from "@angular/core"; +import {environment} from "../../../environments/environment"; +import {Observable} from "rxjs"; +import {HttpClient} from "@angular/common/http"; +import {ResourceActionDto} from "./ResourceActionDto"; + +@Injectable({providedIn: 'root'}) +export class RoleClient { + apiPrefix = environment.coskyRestApiHost + '/roles'; + + constructor(private httpClient: HttpClient) { + + } + + getAllRole(): Observable { + return this.httpClient.get(this.apiPrefix); + } + + saveRole(roleName: string, resourceActionBind: ResourceActionDto[]): Observable { + const apiUrl = `${this.apiPrefix}/${roleName}`; + return this.httpClient.patch(apiUrl, {roleName, resourceActionBind}); + } + + removeRole(roleName: string): Observable { + const apiUrl = `${this.apiPrefix}/${roleName}`; + return this.httpClient.delete(apiUrl); + } +} diff --git a/cosky-dashboard/src/app/api/user/UserClient.ts b/cosky-dashboard/src/app/api/user/UserClient.ts new file mode 100644 index 00000000..8227e11b --- /dev/null +++ b/cosky-dashboard/src/app/api/user/UserClient.ts @@ -0,0 +1,51 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Injectable} from "@angular/core"; +import {environment} from "../../../environments/environment"; +import {HttpClient} from "@angular/common/http"; +import {Observable} from "rxjs"; +import {UserDto} from "./UserDto"; + +@Injectable({providedIn: 'root'}) +export class UserClient { + apiPrefix = environment.coskyRestApiHost + '/users'; + + constructor(private httpClient: HttpClient) { + + } + + query(): Observable { + return this.httpClient.get(this.apiPrefix); + } + + changePwd(username: string, oldPassword: string, newPassword: string): Observable { + const apiUrl = `${this.apiPrefix}/${username}/password`; + return this.httpClient.patch(apiUrl, {username, oldPassword, newPassword}); + } + + saveUser(username: string, password: string): Observable { + const apiUrl = `${this.apiPrefix}/${username}`; + return this.httpClient.put(apiUrl, {username, password}); + } + + removeUser(username: string): Observable { + const apiUrl = `${this.apiPrefix}/${username}`; + return this.httpClient.delete(apiUrl); + } + + bindRole(username: string, roleBind: string[]): Observable { + const apiUrl = `${this.apiPrefix}/${username}`; + return this.httpClient.patch(apiUrl, roleBind); + } + +} diff --git a/cosky-dashboard/src/app/api/user/UserDto.ts b/cosky-dashboard/src/app/api/user/UserDto.ts new file mode 100644 index 00000000..b26b0b7a --- /dev/null +++ b/cosky-dashboard/src/app/api/user/UserDto.ts @@ -0,0 +1,17 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface UserDto { + username: string; + roleBind: string[]; +} diff --git a/cosky-dashboard/src/app/app-routing.module.ts b/cosky-dashboard/src/app/app-routing.module.ts index f45b4c35..ced087ed 100644 --- a/cosky-dashboard/src/app/app-routing.module.ts +++ b/cosky-dashboard/src/app/app-routing.module.ts @@ -17,13 +17,21 @@ import {NamespaceComponent} from './components/namespace/namespace.component'; import {ConfigComponent} from './components/config/config.component'; import {ServiceComponent} from './components/service/service.component'; import {DashboardComponent} from './components/dashboard/dashboard.component'; +import {UserComponent} from "./components/user/user.component"; +import {AuthGuard} from "./security/AuthGuard"; +import {RoleComponent} from "./components/role/role.component"; +import {LoginComponent} from "./components/login/login.component"; const routes: Routes = [ + {path: '', pathMatch: 'full', redirectTo: '/dashboard'}, - {path: 'dashboard', component: DashboardComponent}, - {path: 'namespace', component: NamespaceComponent}, - {path: 'config', component: ConfigComponent}, - {path: 'service', component: ServiceComponent} + {path: 'login', component: LoginComponent}, + {path: 'dashboard', canActivate: [AuthGuard],component: DashboardComponent}, + {path: 'namespace', canActivate: [AuthGuard],component: NamespaceComponent}, + {path: 'config', canActivate: [AuthGuard], component: ConfigComponent}, + {path: 'service', canActivate: [AuthGuard], component: ServiceComponent}, + {path: 'user', canActivate: [AuthGuard], component: UserComponent}, + {path: 'role', canActivate: [AuthGuard], component: RoleComponent} ]; @NgModule({ diff --git a/cosky-dashboard/src/app/app.component.html b/cosky-dashboard/src/app/app.component.html index ba492e4d..fd8ad79d 100644 --- a/cosky-dashboard/src/app/app.component.html +++ b/cosky-dashboard/src/app/app.component.html @@ -40,6 +40,18 @@

{{title}}

Namespace +
  • + +
  • @@ -63,5 +75,4 @@

    {{title}}

    CoSky ©2021
    - diff --git a/cosky-dashboard/src/app/app.module.ts b/cosky-dashboard/src/app/app.module.ts index b2b3d924..421cde4c 100644 --- a/cosky-dashboard/src/app/app.module.ts +++ b/cosky-dashboard/src/app/app.module.ts @@ -20,7 +20,7 @@ import {NZ_I18N, zh_CN} from 'ng-zorro-antd/i18n'; import {registerLocaleData} from '@angular/common'; import zh from '@angular/common/locales/zh'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {HttpClientModule} from '@angular/common/http'; +import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {IconsProviderModule} from './icons-provider.module'; import {NzLayoutModule} from 'ng-zorro-antd/layout'; @@ -54,9 +54,17 @@ import {ConfigVersionListComponent} from './components/config/config-version-lis import {ConfigVersionComponent} from './components/config/config-version/config-version.component'; import {DashboardComponent} from './components/dashboard/dashboard.component'; import {ConfigImporterComponent} from './components/config/config-importer/config-importer.component'; +import {AuthInterceptor} from "./security/AuthInterceptor"; +import { LoginComponent } from './components/login/login.component'; +import { UserComponent } from './components/user/user.component'; +import { RoleComponent } from './components/role/role.component'; registerLocaleData(zh); +export const httpInterceptorProviders = [ + {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, +]; + @NgModule({ declarations: [ AppComponent, @@ -70,7 +78,10 @@ registerLocaleData(zh); ConfigVersionListComponent, ConfigVersionComponent, DashboardComponent, - ConfigImporterComponent + ConfigImporterComponent, + LoginComponent, + UserComponent, + RoleComponent ], imports: [ BrowserModule, @@ -100,7 +111,7 @@ registerLocaleData(zh); MonacoEditorModule, MonacoEditorModule.forRoot() ], - providers: [{provide: NZ_I18N, useValue: zh_CN}], + providers: [{provide: NZ_I18N, useValue: zh_CN}, httpInterceptorProviders], bootstrap: [AppComponent] }) export class AppModule { diff --git a/cosky-dashboard/src/app/components/login/login.component.html b/cosky-dashboard/src/app/components/login/login.component.html new file mode 100644 index 00000000..d6a25581 --- /dev/null +++ b/cosky-dashboard/src/app/components/login/login.component.html @@ -0,0 +1,20 @@ + + diff --git a/cosky-dashboard/src/app/components/login/login.component.scss b/cosky-dashboard/src/app/components/login/login.component.scss new file mode 100644 index 00000000..413653dc --- /dev/null +++ b/cosky-dashboard/src/app/components/login/login.component.scss @@ -0,0 +1,4 @@ +.login-card{ + max-width: 500px; + margin: 200px auto; +} diff --git a/cosky-dashboard/src/app/components/login/login.component.spec.ts b/cosky-dashboard/src/app/components/login/login.component.spec.ts new file mode 100644 index 00000000..d2c0e6c8 --- /dev/null +++ b/cosky-dashboard/src/app/components/login/login.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/cosky-dashboard/src/app/components/login/login.component.ts b/cosky-dashboard/src/app/components/login/login.component.ts new file mode 100644 index 00000000..abefbff6 --- /dev/null +++ b/cosky-dashboard/src/app/components/login/login.component.ts @@ -0,0 +1,31 @@ +import {Component, OnInit} from '@angular/core'; +import {SecurityService} from "../../security/SecurityService"; +import {FormBuilder, FormGroup, Validators} from "@angular/forms"; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent implements OnInit { + loginForm!: FormGroup; + username!: string; + password!: string; + + constructor(private securityService: SecurityService, + private formBuilder: FormBuilder) { + } + + ngOnInit(): void { + const controlsConfig = { + username: [this.username, [Validators.required]], + password: [this.password, [Validators.required]] + }; + this.loginForm = this.formBuilder.group(controlsConfig); + } + + + signIn() { + this.securityService.signIn(this.username, this.password); + } +} diff --git a/cosky-dashboard/src/app/components/role/role.component.html b/cosky-dashboard/src/app/components/role/role.component.html new file mode 100644 index 00000000..2d3aeb98 --- /dev/null +++ b/cosky-dashboard/src/app/components/role/role.component.html @@ -0,0 +1,38 @@ + +
    +
    + +
    +
    + + + + + Role Name + Action + + + + + {{role}} + + + + + + + + diff --git a/cosky-dashboard/src/app/components/role/role.component.scss b/cosky-dashboard/src/app/components/role/role.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/cosky-dashboard/src/app/components/role/role.component.spec.ts b/cosky-dashboard/src/app/components/role/role.component.spec.ts new file mode 100644 index 00000000..5555d93c --- /dev/null +++ b/cosky-dashboard/src/app/components/role/role.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RoleComponent } from './role.component'; + +describe('RoleComponent', () => { + let component: RoleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ RoleComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RoleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/cosky-dashboard/src/app/components/role/role.component.ts b/cosky-dashboard/src/app/components/role/role.component.ts new file mode 100644 index 00000000..3fb4dd64 --- /dev/null +++ b/cosky-dashboard/src/app/components/role/role.component.ts @@ -0,0 +1,39 @@ +import {Component, OnInit} from '@angular/core'; +import {UserDto} from "../../api/user/UserDto"; +import {RoleClient} from "../../api/role/RoleClient"; + +@Component({ + selector: 'app-role', + templateUrl: './role.component.html', + styleUrls: ['./role.component.scss'] +}) +export class RoleComponent implements OnInit { + roles: string[] = []; + + constructor(private roleClient: RoleClient) { + } + + loadRoles() { + this.roleClient.getAllRole().subscribe(resp => { + this.roles = resp; + }) + } + + ngOnInit(): void { + this.loadRoles(); + } + + removeRole(role: string) { + this.roleClient.removeRole(role).subscribe(resp => { + this.loadRoles(); + }) + } + + isSystem(role: string) { + return 'admin' === role; + } + + showAddRole() { + + } +} diff --git a/cosky-dashboard/src/app/components/user/user.component.html b/cosky-dashboard/src/app/components/user/user.component.html new file mode 100644 index 00000000..0d651788 --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user.component.html @@ -0,0 +1,39 @@ + +
    +
    + +
    +
    + + + + + Username + Role + Action + + + + + {{user.username}} + {{user.roleBind|json}} + + + + + + + diff --git a/cosky-dashboard/src/app/components/user/user.component.scss b/cosky-dashboard/src/app/components/user/user.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/cosky-dashboard/src/app/components/user/user.component.spec.ts b/cosky-dashboard/src/app/components/user/user.component.spec.ts new file mode 100644 index 00000000..e6bf5969 --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserComponent } from './user.component'; + +describe('UserComponent', () => { + let component: UserComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/cosky-dashboard/src/app/components/user/user.component.ts b/cosky-dashboard/src/app/components/user/user.component.ts new file mode 100644 index 00000000..d972c7ce --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user.component.ts @@ -0,0 +1,39 @@ +import {Component, OnInit} from '@angular/core'; +import {UserDto} from "../../api/user/UserDto"; +import {UserClient} from "../../api/user/UserClient"; + +@Component({ + selector: 'app-user', + templateUrl: './user.component.html', + styleUrls: ['./user.component.scss'] +}) +export class UserComponent implements OnInit { + users: UserDto[] = []; + + constructor(private userClient: UserClient) { + } + + loadUsers() { + this.userClient.query().subscribe(resp => { + this.users = resp; + }) + } + + ngOnInit(): void { + this.loadUsers(); + } + + removeUser(user: UserDto) { + this.userClient.removeUser(user.username).subscribe(resp => { + this.loadUsers(); + }) + } + + isSystem(user: UserDto) { + return 'cosky' === user.username; + } + + showAddUser() { + + } +} diff --git a/cosky-dashboard/src/app/security/AuthGuard.ts b/cosky-dashboard/src/app/security/AuthGuard.ts new file mode 100644 index 00000000..b98184bc --- /dev/null +++ b/cosky-dashboard/src/app/security/AuthGuard.ts @@ -0,0 +1,51 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from "@angular/router"; +import {Observable} from "rxjs"; +import {SecurityService} from "./SecurityService"; +import {Injectable} from "@angular/core"; +import {NzMessageService} from "ng-zorro-antd/message"; +import {AuthenticateClient} from "../api/authenticate/AuthenticateClient"; +import {map} from "rxjs/operators"; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + + constructor(private securityService: SecurityService + , private router: Router + , private messageService: NzMessageService) { + } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) + : Observable | Promise | boolean | UrlTree { + if (this.securityService.authenticated()) { + return true; + } + if (this.securityService.refreshValid()) { + return this.securityService.refreshToken() + .pipe(map(succeeded => { + if (!succeeded) { + this.securityService.redirectFrom = route.url[0].path; + this.router.navigateByUrl("login") + } + return succeeded; + })); + } + this.messageService.error(`UNAUTHORIZED.`); + this.securityService.redirectFrom = route.url[0].path; + return this.router.parseUrl("login"); + } +} diff --git a/cosky-dashboard/src/app/security/AuthInterceptor.ts b/cosky-dashboard/src/app/security/AuthInterceptor.ts new file mode 100644 index 00000000..bd595e09 --- /dev/null +++ b/cosky-dashboard/src/app/security/AuthInterceptor.ts @@ -0,0 +1,41 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http"; +import {Observable, of} from "rxjs"; +import {Injectable} from "@angular/core"; +import {SecurityService} from "./SecurityService"; +import {Router} from "@angular/router"; +import {catchError} from "rxjs/operators"; +import {NzMessageService} from 'ng-zorro-antd/message'; + +const UNAUTHORIZED = 401; +const FORBIDDEN = 403; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + + constructor(private securityService: SecurityService, private router: Router, private messageService: NzMessageService) { + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + let accessToken = this.securityService.getAccessToken(); + + const authReq = req.clone({ + headers: req.headers.set('Authorization', accessToken) + }); + + return next.handle(authReq); + } + +} diff --git a/cosky-dashboard/src/app/security/SecurityService.ts b/cosky-dashboard/src/app/security/SecurityService.ts new file mode 100644 index 00000000..b4595882 --- /dev/null +++ b/cosky-dashboard/src/app/security/SecurityService.ts @@ -0,0 +1,128 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from "@angular/core"; + +import {Token} from "../api/authenticate/Token"; +import {TokenPayload} from "../api/authenticate/TokenPayload"; +import {AuthenticateClient} from "../api/authenticate/AuthenticateClient"; +import {Observable, of, throwError} from "rxjs"; +import {catchError, map} from "rxjs/operators"; +import {HttpErrorResponse} from "@angular/common/http"; +import {NzMessageService} from "ng-zorro-antd/message"; +import {Router} from "@angular/router"; + +const ACCESS_TOKEN_KEY = "cosky:accessToken" +const REFRESH_TOKEN_KEY = "cosky:refreshToken" + + +@Injectable({providedIn: 'root'}) +export class SecurityService { + redirectFrom: string = 'dashboard'; + + constructor(private authenticateClient: AuthenticateClient + , private messageService: NzMessageService + , private router: Router) { + } + + signIn(username: string, password: string) { + this.authenticateClient.login(username, password).subscribe((resp) => { + this.setToken(resp); + this.router.navigate([this.redirectFrom]) + }, + ((errorResponse: HttpErrorResponse) => { + if (errorResponse.error) { + this.messageService.error(errorResponse.error.msg) + } + console.error(errorResponse) + })) + } + + getAccessToken(): string { + let accessToken = localStorage.getItem(ACCESS_TOKEN_KEY) + if (accessToken) { + return accessToken; + } + return ''; + } + + setToken(token: Token) { + localStorage.setItem(ACCESS_TOKEN_KEY, token.accessToken); + localStorage.setItem(REFRESH_TOKEN_KEY, token.refreshToken); + } + + authenticated(): boolean { + const accessToken = this.getAccessToken(); + if (accessToken.length === 0) { + return false; + } + return this.isValidity(accessToken); + } + + getCurrentTimeOfSecond() { + return Date.now() / 1000; + } + + isValidity(token: string) { + const tokenExp = this.parseToken(token).exp; + return tokenExp > this.getCurrentTimeOfSecond(); + } + + parseToken(token: string): TokenPayload { + let tokenSplit = token.split("."); + /** + * check tokenSplit.length===3 + * + * TODO atob bug + * Uncaught DOMException: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded. + * at :1:1 + */ + const payloadStr = atob(tokenSplit[1]); + + return JSON.parse(payloadStr); + } + + refreshValid(): boolean { + const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY); + if (!refreshToken) { + return false; + } + return this.isValidity(refreshToken); + } + + refreshToken(): Observable { + const accessToken = localStorage.getItem(ACCESS_TOKEN_KEY); + const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY); + if (!accessToken || !refreshToken) { + return throwError('accessToken or refreshToken is empty!') + } + return this.authenticateClient + .refresh(accessToken, refreshToken) + .pipe( + map(resp => { + this.setToken(resp); + return true; + }), + catchError((err, caught) => { + console.log(err); + return of(false); + }) + ); + } + + signOut() { + localStorage.clear(); + this.router.navigate(['login']) + } + +} diff --git a/cosky-dashboard/src/index.html b/cosky-dashboard/src/index.html index ae489152..8d642344 100644 --- a/cosky-dashboard/src/index.html +++ b/cosky-dashboard/src/index.html @@ -16,7 +16,8 @@ CoSky Dashboard - + + diff --git a/cosky-dependencies/build.gradle.kts b/cosky-dependencies/build.gradle.kts index b49db41b..c27da00a 100644 --- a/cosky-dependencies/build.gradle.kts +++ b/cosky-dependencies/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { api(platform("org.springframework.boot:spring-boot-dependencies:${rootProject.ext.get("springBootVersion")}")) api(platform("org.springframework.cloud:spring-cloud-dependencies:${rootProject.ext.get("springCloudVersion")}")) -// api(platform("me.ahoo.cosid:cosid-bom:${rootProject.ext.get("cosIdVersion")}")) + api(platform("me.ahoo.cosid:cosid-bom:${rootProject.ext.get("cosIdVersion")}")) constraints { api("org.projectlombok:lombok:${rootProject.ext.get("lombokVersion")}") api("com.google.guava:guava:${rootProject.ext.get("guavaVersion")}") diff --git a/cosky-discovery/src/main/java/me/ahoo/cosky/discovery/DiscoveryKeyGenerator.java b/cosky-discovery/src/main/java/me/ahoo/cosky/discovery/DiscoveryKeyGenerator.java index f46a5ab0..07420510 100644 --- a/cosky-discovery/src/main/java/me/ahoo/cosky/discovery/DiscoveryKeyGenerator.java +++ b/cosky-discovery/src/main/java/me/ahoo/cosky/discovery/DiscoveryKeyGenerator.java @@ -15,7 +15,7 @@ import com.google.common.base.Strings; import lombok.var; -import me.ahoo.cosky.core.Consts; +import me.ahoo.cosky.core.CoSky; /** * @author ahoo wang @@ -66,7 +66,7 @@ public static String getServiceStatKey(String namespace) { } public static String getNamespaceOfKey(String key) { - var firstSplitIdx = key.indexOf(Consts.KEY_SEPARATOR); + var firstSplitIdx = key.indexOf(CoSky.KEY_SEPARATOR); return key.substring(0, firstSplitIdx); } diff --git a/cosky-discovery/src/test/java/me/ahoo/cosky/discovery/DiscoveryKeyGeneratorTest.java b/cosky-discovery/src/test/java/me/ahoo/cosky/discovery/DiscoveryKeyGeneratorTest.java index 9b70f384..74ad3ab5 100644 --- a/cosky-discovery/src/test/java/me/ahoo/cosky/discovery/DiscoveryKeyGeneratorTest.java +++ b/cosky-discovery/src/test/java/me/ahoo/cosky/discovery/DiscoveryKeyGeneratorTest.java @@ -14,7 +14,7 @@ package me.ahoo.cosky.discovery; import lombok.var; -import me.ahoo.cosky.core.Consts; +import me.ahoo.cosky.core.CoSky; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -25,19 +25,19 @@ public class DiscoveryKeyGeneratorTest { @Test public void getServiceIdxKey() { - var serviceIdxKey = DiscoveryKeyGenerator.getServiceIdxKey(Consts.COSKY); + var serviceIdxKey = DiscoveryKeyGenerator.getServiceIdxKey(CoSky.COSKY); Assertions.assertEquals("cosky:svc_idx", serviceIdxKey); } @Test public void getServiceInstanceIdxKey() { - var serviceIdxKey = DiscoveryKeyGenerator.getInstanceIdxKey(Consts.COSKY, "order_service"); + var serviceIdxKey = DiscoveryKeyGenerator.getInstanceIdxKey(CoSky.COSKY, "order_service"); Assertions.assertEquals("cosky:svc_itc_idx:order_service", serviceIdxKey); } @Test public void getInstanceKey() { - var instanceKey = DiscoveryKeyGenerator.getInstanceKey(Consts.COSKY, "http#127.0.0.1#8080@order_service"); + var instanceKey = DiscoveryKeyGenerator.getInstanceKey(CoSky.COSKY, "http#127.0.0.1#8080@order_service"); Assertions.assertEquals("cosky:svc_itc:http#127.0.0.1#8080@order_service", instanceKey); } } diff --git a/cosky-rest-api/build.gradle.kts b/cosky-rest-api/build.gradle.kts index cda5992d..5b8bb647 100644 --- a/cosky-rest-api/build.gradle.kts +++ b/cosky-rest-api/build.gradle.kts @@ -66,10 +66,13 @@ dependencies { implementation(project(":spring-cloud-starter-cosky-config")) implementation(project(":spring-cloud-starter-cosky-discovery")) implementation("com.google.guava:guava") + implementation("me.ahoo.cosid:cosid-redis") + implementation("me.ahoo.cosid:spring-boot-starter-cosid") + implementation("org.springframework.boot:spring-boot-starter-web") -// implementation("org.springframework.cloud:spring-cloud-starter-openfeign") -// implementation("io.dropwizard.metrics:metrics-core") -// implementation("io.dropwizard.metrics:metrics-jvm") + implementation("io.jsonwebtoken:jjwt-api:${rootProject.ext.get("jjwtVersion")}") + implementation("io.jsonwebtoken:jjwt-impl:${rootProject.ext.get("jjwtVersion")}") + implementation("io.jsonwebtoken:jjwt-jackson:${rootProject.ext.get("jjwtVersion")}") compileOnly("org.projectlombok:lombok:${rootProject.ext.get("lombokVersion")}") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:${rootProject.ext.get("springBootVersion")}") annotationProcessor("org.projectlombok:lombok:${rootProject.ext.get("lombokVersion")}") diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/AppConfig.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/AppConfig.java index ae757b2b..4aa96208 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/AppConfig.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/AppConfig.java @@ -21,4 +21,7 @@ @Configuration public class AppConfig { + + + } diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/DashboardConfig.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/DashboardConfig.java index a0d25893..64669980 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/DashboardConfig.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/DashboardConfig.java @@ -26,6 +26,8 @@ */ @Component public class DashboardConfig implements ErrorPageRegistrar { + public static final String INDEX_PAGE = "/dashboard/index.html"; + /** * Register pages as required with the given registry. * @@ -33,7 +35,7 @@ public class DashboardConfig implements ErrorPageRegistrar { */ @Override public void registerErrorPages(ErrorPageRegistry registry) { - ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/index.html"); + ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, INDEX_PAGE); registry.addErrorPages(error404Page); } } diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/SwaggerConfig.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/SwaggerConfig.java new file mode 100644 index 00000000..989074cf --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/config/SwaggerConfig.java @@ -0,0 +1,73 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.config; + +import me.ahoo.cosky.rest.security.AuthorizeHandlerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * @author ahoo wang + */ +@Configuration +public class SwaggerConfig { + @Bean + public Docket api() { + return new Docket(DocumentationType.OAS_30) + .apiInfo(apiInfo()) + .securityContexts(Arrays.asList(securityContext())) + .securitySchemes(Arrays.asList(apiKey())) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfo( + "CoSky REST API", + "High-performance, low-cost microservice governance platform. Service Discovery and Configuration Service.", + "1.0", + "https://github.com/Ahoo-Wang", + new Contact("ahoo wang", "https://github.com/Ahoo-Wang", "ahoowang@qq.com"), + "Apache-2.0", + "https://github.com/Ahoo-Wang/CoSky/blob/main/LICENSE", + Collections.emptyList()); + } + + private SecurityContext securityContext() { + return SecurityContext.builder().securityReferences(defaultAuth()).build(); + } + + private List defaultAuth() { + AuthorizationScope authorizationScope = new AuthorizationScope("global", ""); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + return Arrays.asList(new SecurityReference(AuthorizeHandlerInterceptor.AUTH_HEADER, authorizationScopes)); + } + + private ApiKey apiKey() { + return new ApiKey(AuthorizeHandlerInterceptor.AUTH_HEADER, AuthorizeHandlerInterceptor.AUTH_HEADER, "header"); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/AuthenticateController.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/AuthenticateController.java new file mode 100644 index 00000000..232640a9 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/AuthenticateController.java @@ -0,0 +1,49 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.controller; + +import me.ahoo.cosky.rest.dto.user.LoginRequest; +import me.ahoo.cosky.rest.dto.user.LoginResponse; +import me.ahoo.cosky.rest.dto.user.RefreshRequest; +import me.ahoo.cosky.rest.security.JwtProvider; +import me.ahoo.cosky.rest.security.user.UserService; +import me.ahoo.cosky.rest.support.RequestPathPrefix; +import org.springframework.web.bind.annotation.*; + +/** + * @author ahoo wang + */ +@CrossOrigin("*") +@RestController +@RequestMapping(RequestPathPrefix.AUTHENTICATE_PREFIX) +public class AuthenticateController { + + private final JwtProvider jwtProvider; + private final UserService userService; + + public AuthenticateController(JwtProvider jwtProvider, UserService userService) { + this.jwtProvider = jwtProvider; + this.userService = userService; + } + + @PostMapping("/login") + public LoginResponse login(@RequestBody LoginRequest loginRequest) { + return userService.login(loginRequest.getUsername(), loginRequest.getPassword()); + } + + @PostMapping("/refresh") + public LoginResponse refresh(@RequestBody RefreshRequest refreshRequest) { + return jwtProvider.refresh(refreshRequest.getAccessToken(), refreshRequest.getRefreshToken()); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/ConfigController.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/ConfigController.java index 2d6e4ced..2dea5b48 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/ConfigController.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/ConfigController.java @@ -22,7 +22,7 @@ import me.ahoo.cosky.config.ConfigHistory; import me.ahoo.cosky.config.ConfigService; import me.ahoo.cosky.config.ConfigVersion; -import me.ahoo.cosky.core.Consts; +import me.ahoo.cosky.core.CoSky; import me.ahoo.cosky.rest.dto.config.ImportResponse; import me.ahoo.cosky.rest.support.RequestPathPrefix; import me.ahoo.cosky.rest.util.Zips; @@ -136,7 +136,7 @@ public CompletableFuture> exportZip(@PathVariable String return CompletableFuture.allOf(getConfigFutures).thenApply(nil -> { HttpHeaders headers = new HttpHeaders(); - String fileName = Consts.COSKY + "_export_config_" + System.currentTimeMillis() / 1000 + ".zip"; + String fileName = CoSky.COSKY + "_export_config_" + System.currentTimeMillis() / 1000 + ".zip"; headers.add("Content-Disposition", "attachment;filename=" + fileName); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); return new ResponseEntity<>(Zips.zip(zipItems), headers, HttpStatus.OK); diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/NamespaceController.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/NamespaceController.java index f1907900..d1f2d1e4 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/NamespaceController.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/NamespaceController.java @@ -15,6 +15,7 @@ import me.ahoo.cosky.core.NamespaceService; import me.ahoo.cosky.core.NamespacedContext; +import me.ahoo.cosky.rest.security.rbac.annotation.AdminResource; import me.ahoo.cosky.rest.support.RequestPathPrefix; import org.springframework.web.bind.annotation.*; @@ -35,9 +36,9 @@ public NamespaceController(NamespaceService namespaceService) { this.namespaceService = namespaceService; } - @GetMapping public CompletableFuture> getNamespaces() { + return namespaceService.getNamespaces(); } @@ -46,16 +47,19 @@ public String current() { return NamespacedContext.GLOBAL.getNamespace(); } + @AdminResource @PutMapping(RequestPathPrefix.NAMESPACES_CURRENT_NAMESPACE) public void setCurrentContextNamespace(@PathVariable String namespace) { NamespacedContext.GLOBAL.setCurrentContextNamespace(namespace); } + @AdminResource @PutMapping(RequestPathPrefix.NAMESPACES_NAMESPACE) public CompletableFuture setNamespace(@PathVariable String namespace) { return namespaceService.setNamespace(namespace); } + @AdminResource @DeleteMapping(RequestPathPrefix.NAMESPACES_NAMESPACE) public CompletableFuture removeNamespace(@PathVariable String namespace) { return namespaceService.removeNamespace(namespace); diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/RoleController.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/RoleController.java new file mode 100644 index 00000000..f5ce6626 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/RoleController.java @@ -0,0 +1,53 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.controller; + +import me.ahoo.cosky.rest.dto.role.ResourceActionDto; +import me.ahoo.cosky.rest.security.rbac.RBACService; +import me.ahoo.cosky.rest.security.rbac.ResourceAction; +import me.ahoo.cosky.rest.security.rbac.annotation.AdminResource; +import me.ahoo.cosky.rest.support.RequestPathPrefix; +import org.springframework.web.bind.annotation.*; + +import java.util.Set; + +/** + * @author ahoo wang + */ +@CrossOrigin("*") +@RestController +@RequestMapping(RequestPathPrefix.ROLES_PREFIX) +@AdminResource +public class RoleController { + private final RBACService rbacService; + + public RoleController(RBACService rbacService) { + this.rbacService = rbacService; + } + + @GetMapping + public Set getAllRole() { + return rbacService.getAllRole(); + } + + @PutMapping("/{roleName}") + public void saveRole(@PathVariable String roleName,@RequestBody Set resourceActionBind) { + rbacService.saveRole(roleName, resourceActionBind); + } + + @DeleteMapping("/{roleName}") + public void removeRole(@PathVariable String roleName) { + rbacService.removeRole(roleName); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/UserController.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/UserController.java new file mode 100644 index 00000000..d91bc266 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/UserController.java @@ -0,0 +1,67 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.controller; + +import me.ahoo.cosky.rest.dto.user.AddUserRequest; +import me.ahoo.cosky.rest.dto.user.ChangePwdRequest; +import me.ahoo.cosky.rest.security.rbac.annotation.AdminResource; +import me.ahoo.cosky.rest.security.user.User; +import me.ahoo.cosky.rest.security.user.UserService; +import me.ahoo.cosky.rest.support.RequestPathPrefix; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Set; + +/** + * @author ahoo wang + */ +@CrossOrigin("*") +@RestController +@RequestMapping(RequestPathPrefix.USERS_PREFIX) +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public List query() { + return userService.query(); + } + + @PatchMapping("/{username}/password") + public boolean changePwd(@PathVariable String username, @RequestBody ChangePwdRequest changePwdRequest) { + return userService.changePwd(username, changePwdRequest.getOldPassword(), changePwdRequest.getNewPassword()); + } + + @AdminResource + @PostMapping("/") + public boolean addUser(@RequestBody AddUserRequest addUserRequest) { + return userService.addUser(addUserRequest.getUsername(), addUserRequest.getPassword()); + } + + @AdminResource + @PatchMapping("/{username}/role") + public void bindRole(@PathVariable String username, @RequestBody Set roleBind) { + userService.bindRole(username, roleBind); + } + + @AdminResource + @DeleteMapping("/{username}") + public boolean removeUser(@PathVariable String username) { + return userService.removeUser(username); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/user/User.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/role/ResourceActionDto.java similarity index 59% rename from cosky-rest-api/src/main/java/me/ahoo/cosky/rest/user/User.java rename to cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/role/ResourceActionDto.java index 9c4553fa..e7250d19 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/user/User.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/role/ResourceActionDto.java @@ -11,39 +11,36 @@ * limitations under the License. */ -package me.ahoo.cosky.rest.user; - -import me.ahoo.cosky.core.Consts; +package me.ahoo.cosky.rest.dto.role; /** * @author ahoo wang */ -public class User { - public static String SUPER_USER = Consts.COSKY; - - private String userName; - private String pwd; +public class ResourceActionDto { + private String namespace; + private String action; - public String getUserName() { - return userName; + public String getNamespace() { + return namespace; } - public void setUserName(String userName) { - this.userName = userName; + public void setNamespace(String namespace) { + this.namespace = namespace; } - public String getPwd() { - return pwd; + public String getAction() { + return action; } - public void setPwd(String pwd) { - this.pwd = pwd; + public void setAction(String action) { + this.action = action; } @Override public String toString() { - return "User{" + - "userName='" + userName + '\'' + + return "ResourceActionDto{" + + "namespace='" + namespace + '\'' + + ", action='" + action + '\'' + '}'; } } diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/UserRoleBinding.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/AddUserRequest.java similarity index 61% rename from cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/UserRoleBinding.java rename to cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/AddUserRequest.java index ce7c4ec0..41a152db 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/UserRoleBinding.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/AddUserRequest.java @@ -11,31 +11,28 @@ * limitations under the License. */ -package me.ahoo.cosky.rest.rbac; - -import java.util.Set; +package me.ahoo.cosky.rest.dto.user; /** * @author ahoo wang */ -public class UserRoleBinding { - - private String userName; - private Set roleBind; +public class AddUserRequest { + private String username; + private String password; - public String getUserName() { - return userName; + public String getUsername() { + return username; } - public void setUserName(String userName) { - this.userName = userName; + public void setUsername(String username) { + this.username = username; } - public Set getRoleBind() { - return roleBind; + public String getPassword() { + return password; } - public void setRoleBind(Set roleBind) { - this.roleBind = roleBind; + public void setPassword(String password) { + this.password = password; } } diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/RBACService.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/ChangePwdRequest.java similarity index 59% rename from cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/RBACService.java rename to cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/ChangePwdRequest.java index 28fe283f..190175d6 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/RBACService.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/ChangePwdRequest.java @@ -11,40 +11,28 @@ * limitations under the License. */ -package me.ahoo.cosky.rest.rbac; - -import java.util.Set; +package me.ahoo.cosky.rest.dto.user; /** - * TODO - * 认证 -> 授权 -> 鉴权 -> 权限控制 - * * @author ahoo wang */ -public class RBACService { - - public void saveRole(String roleName, Set resourceActionBind) { +public class ChangePwdRequest { + private String oldPassword; + private String newPassword; + public String getOldPassword() { + return oldPassword; } - public void removeRole(String roleName) { - + public void setOldPassword(String oldPassword) { + this.oldPassword = oldPassword; } - public void userRoleBind(String userName, Set roleBind) { - + public String getNewPassword() { + return newPassword; } - /** - * 鉴权 - */ - public void authenticate() { - } - - /** - * 权限控制 - */ - public void accessCheck(){ - + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; } } diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/user/UserService.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/LoginRequest.java similarity index 61% rename from cosky-rest-api/src/main/java/me/ahoo/cosky/rest/user/UserService.java rename to cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/LoginRequest.java index 8cc07460..5fcfdb2a 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/user/UserService.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/LoginRequest.java @@ -11,30 +11,28 @@ * limitations under the License. */ -package me.ahoo.cosky.rest.user; - -import me.ahoo.cosky.rest.dto.user.AuthorizeResponse; +package me.ahoo.cosky.rest.dto.user; /** - * TODO - * * @author ahoo wang */ -public class UserService { +public class LoginRequest { + private String username; + private String password; + + public String getUsername() { + return username; + } - public Boolean saveUser(String userName, String pwd) { - return Boolean.TRUE; + public void setUsername(String username) { + this.username = username; } - public Boolean removeUser(String userName) { - return Boolean.TRUE; + public String getPassword() { + return password; } - /** - * @param userName - * @param pwd - */ - public AuthorizeResponse authorize(String userName, String pwd) { - return null; + public void setPassword(String password) { + this.password = password; } } diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/AuthorizeResponse.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/LoginResponse.java similarity index 97% rename from cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/AuthorizeResponse.java rename to cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/LoginResponse.java index 17ae80b2..b19446f1 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/AuthorizeResponse.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/LoginResponse.java @@ -16,7 +16,8 @@ /** * @author ahoo wang */ -public class AuthorizeResponse { +public class LoginResponse { + private String accessToken; private String refreshToken; diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/RefreshRequest.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/RefreshRequest.java new file mode 100644 index 00000000..8a6a872e --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/user/RefreshRequest.java @@ -0,0 +1,20 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.dto.user; + +/** + * @author ahoo wang + */ +public class RefreshRequest extends LoginResponse{ +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/Role.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/Role.java deleted file mode 100644 index 9e1223c9..00000000 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/rbac/Role.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.ahoo.cosky.rest.rbac; - -import com.google.common.base.Objects; - -import java.util.Set; - -/** - * @author ahoo wang - */ -public class Role { - - private String roleName; - - private Set resourceActionBind; - - public String getRoleName() { - return roleName; - } - - public void setRoleName(String roleName) { - this.roleName = roleName; - } - - public Set getResourceActionBind() { - return resourceActionBind; - } - - public void setResourceActionBind(Set resourceActionBind) { - this.resourceActionBind = resourceActionBind; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Role)) return false; - Role role = (Role) o; - return Objects.equal(roleName, role.roleName); - } - - @Override - public int hashCode() { - return Objects.hashCode(roleName); - } - - public static class ResourceAction { - private String namespace; - private Action action; - - public String getNamespace() { - return namespace; - } - - public void setNamespace(String namespace) { - this.namespace = namespace; - } - - public Action getAction() { - return action; - } - - public void setAction(Action action) { - this.action = action; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof ResourceAction)) return false; - ResourceAction that = (ResourceAction) o; - return Objects.equal(namespace, that.namespace) && action == that.action; - } - - @Override - public int hashCode() { - return Objects.hashCode(namespace, action); - } - - @Override - public String toString() { - return "ResourceAction{" + - "namespace='" + namespace + '\'' + - ", action=" + action + - '}'; - } - } - - public enum Action { - READ("r"), - WRITE("w"), - READ_WRITE("rw"); - - private final String value; - - Action(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - Action of(String value) { - switch (value) { - case "r": { - return READ; - } - case "w": { - return WRITE; - } - case "rw": { - return READ_WRITE; - } - default: - throw new IllegalStateException("Unexpected value: " + value); - } - } - } - -} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/AuthorizeHandlerInterceptor.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/AuthorizeHandlerInterceptor.java new file mode 100644 index 00000000..4d862058 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/AuthorizeHandlerInterceptor.java @@ -0,0 +1,77 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security; + +import com.google.common.base.Strings; +import me.ahoo.cosky.rest.security.rbac.RBACService; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author ahoo wang + */ +public class AuthorizeHandlerInterceptor implements HandlerInterceptor { + public static final String AUTH_HEADER = "Authorization"; + private final RBACService rbacService; + + public AuthorizeHandlerInterceptor(RBACService rbacService) { + this.rbacService = rbacService; + } + + /** + * Intercept the execution of a handler. Called after HandlerMapping determined + * an appropriate handler object, but before HandlerAdapter invokes the handler. + *

    DispatcherServlet processes a handler in an execution chain, consisting + * of any number of interceptors, with the handler itself at the end. + * With this method, each interceptor can decide to abort the execution chain, + * typically sending an HTTP error or writing a custom response. + *

    Note: special considerations apply for asynchronous + * request processing. For more details see + * {@link AsyncHandlerInterceptor}. + *

    The default implementation returns {@code true}. + * + * @param request current HTTP request + * @param response current HTTP response + * @param handler chosen handler to execute, for type and/or instance evaluation + * @return {@code true} if the execution chain should proceed with the + * next interceptor or the handler itself. Else, DispatcherServlet assumes + * that this interceptor has already dealt with the response itself. + * @throws Exception in case of errors + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if(HttpMethod.OPTIONS.name().equals(request.getMethod())){ + return true; + } + String accessToken = request.getHeader(AUTH_HEADER); + + if (Strings.isNullOrEmpty(accessToken)) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return false; + } + + if (!rbacService.authorize(accessToken, request, (HandlerMethod) handler)) { + response.setStatus(HttpStatus.FORBIDDEN.value()); + return false; + } + + return true; + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/ConditionalOnSecurityEnabled.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/ConditionalOnSecurityEnabled.java new file mode 100644 index 00000000..6948d036 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/ConditionalOnSecurityEnabled.java @@ -0,0 +1,31 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author ahoo wang + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@ConditionalOnProperty(value = ConditionalOnSecurityEnabled.ENABLED_KEY, matchIfMissing = true) +public @interface ConditionalOnSecurityEnabled { + String ENABLED_KEY = SecurityProperties.PREFIX + ".enabled"; +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/JwtProvider.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/JwtProvider.java new file mode 100644 index 00000000..c1b474e1 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/JwtProvider.java @@ -0,0 +1,132 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security; + +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import me.ahoo.cosid.snowflake.SnowflakeFriendlyId; +import me.ahoo.cosky.rest.dto.user.LoginResponse; +import me.ahoo.cosky.rest.security.user.User; + +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.util.Date; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author ahoo wang + */ +public class JwtProvider { + private final static String ROLE = "role"; + private final static String ROLE_SEPARATOR = ","; + private final SecurityProperties.Jwt jwt; + private final Key signingKey; + private final SnowflakeFriendlyId idGenerator; + private final JwtParser jwtParser; + + public JwtProvider(SecurityProperties.Jwt jwt, SnowflakeFriendlyId idGenerator) { + this.jwt = jwt; + this.idGenerator = idGenerator; + final byte[] signingKeyBytes = jwt.getSigningKey().getBytes(Charsets.UTF_8); + this.signingKey = new SecretKeySpec(signingKeyBytes, jwt.getAlgorithm()); + this.jwtParser = Jwts.parserBuilder() + .setSigningKey(signingKey) + .build(); + } + + public static String roleBindAsString(Set roleBind) { + return Joiner + .on(ROLE_SEPARATOR) + .skipNulls() + .join(roleBind); + } + + public static Set stringAsRoleBind(String roleBindStr) { + return Splitter + .on(ROLE_SEPARATOR) + .omitEmptyStrings() + .splitToStream(roleBindStr) + .collect(Collectors.toSet()); + } + + public LoginResponse generateToken(User user) { + String accessTokenId = idGenerator.friendlyId().toString(); + Date now = new Date(); + Date accessTokenExp = new Date(now.getTime() + jwt.getAccessTokenValidity().toMillis()); + String accessToken = Jwts.builder() + .setId(accessTokenId) + .setSubject(user.getUsername()) + .claim(ROLE, roleBindAsString(user.getRoleBind())) + .signWith(signingKey) + .setIssuedAt(now) + .setExpiration(accessTokenExp) + .compact(); + + String refreshTokenId = idGenerator.friendlyId().toString(); + Date refreshTokenExp = new Date(now.getTime() + jwt.getRefreshTokenValidity().toMillis()); + String refreshToken = Jwts.builder() + .setId(refreshTokenId) + .setSubject(accessTokenId) + .signWith(signingKey) + .setIssuedAt(now) + .setExpiration(refreshTokenExp) + .compact(); + LoginResponse authenticateResponse = new LoginResponse(); + authenticateResponse.setAccessToken(accessToken); + authenticateResponse.setRefreshToken(refreshToken); + return authenticateResponse; + } + + public LoginResponse refresh(String accessToken, String refreshToken) { + Claims refreshTokenClaims = decode(refreshToken); + Claims accessTokenClaims; + try { + accessTokenClaims = decode(accessToken); + } catch (ExpiredJwtException expiredJwtException) { + accessTokenClaims = expiredJwtException.getClaims(); + } + + if (!refreshTokenClaims.getSubject().equals(accessTokenClaims.getId())) { + throw new IllegalArgumentException("Illegal refreshToken "); + } + User user = parseUser(accessTokenClaims); + return generateToken(user); + } + + public User authorize(String accessToken) { + Claims claims = decode(accessToken); + return parseUser(claims); + } + + private User parseUser(Claims claims) { + User user = new User(); + user.setUsername(claims.getSubject()); + String roleBindStr = claims.get(ROLE, String.class); + user.setRoleBind(stringAsRoleBind(roleBindStr)); + return user; + } + + + public Claims decode(String jwtToken) throws ExpiredJwtException { + return jwtParser + .parseClaimsJws(jwtToken) + .getBody(); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityCommand.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityCommand.java new file mode 100644 index 00000000..fc1232ef --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityCommand.java @@ -0,0 +1,43 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security; + +import me.ahoo.cosky.rest.security.user.UserService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +/** + * @author ahoo wang + */ +@Component +public class SecurityCommand implements CommandLineRunner { + private final SecurityProperties securityProperties; + private final UserService userService; + + public SecurityCommand(SecurityProperties securityProperties, UserService userService) { + this.securityProperties = securityProperties; + this.userService = userService; + } + + /** + * Callback used to run the bean. + * + * @param args incoming main method arguments + * @throws Exception on error + */ + @Override + public void run(String... args) throws Exception { + userService.initRoot(securityProperties.isEnforceInitSuperUser()); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityContext.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityContext.java new file mode 100644 index 00000000..1378c47f --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityContext.java @@ -0,0 +1,32 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security; + +import me.ahoo.cosky.rest.security.user.User; + +/** + * @author ahoo wang + */ +public class SecurityContext { + private static ThreadLocal currentUser = new ThreadLocal<>(); + + public static User getUser() { + return currentUser.get(); + } + + public static void setUser(User user) { + currentUser.set(user); + + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityException.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityException.java new file mode 100644 index 00000000..ed5f2fb1 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityException.java @@ -0,0 +1,94 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security; + +import me.ahoo.cosky.core.CoskyException; + +/** + * @author ahoo wang + */ +public class SecurityException extends CoskyException { + /** + * Constructs a new runtime exception with {@code null} as its + * detail message. The cause is not initialized, and may subsequently be + * initialized by a call to {@link #initCause}. + */ + public SecurityException() { + } + + /** + * Constructs a new runtime exception with the specified detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public SecurityException(String message) { + super(message); + } + + /** + * Constructs a new runtime exception with the specified detail message and + * cause.

    Note that the detail message associated with + * {@code cause} is not automatically incorporated in + * this runtime exception's detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A null value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public SecurityException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new runtime exception with the specified cause and a + * detail message of (cause==null ? null : cause.toString()) + * (which typically contains the class and detail message of + * cause). This constructor is useful for runtime exceptions + * that are little more than wrappers for other throwables. + * + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A null value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public SecurityException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new runtime exception with the specified detail + * message, cause, suppression enabled or disabled, and writable + * stack trace enabled or disabled. + * + * @param message the detail message. + * @param cause the cause. (A {@code null} value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @param enableSuppression whether or not suppression is enabled + * or disabled + * @param writableStackTrace whether or not the stack trace should + * be writable + * @since 1.7 + */ + public SecurityException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityProperties.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityProperties.java new file mode 100644 index 00000000..1fe80b86 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/SecurityProperties.java @@ -0,0 +1,98 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security; + +import me.ahoo.cosky.core.CoSky; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + + +/** + * @author ahoo wang + */ +@ConfigurationProperties(SecurityProperties.PREFIX) +public class SecurityProperties { + public static final String PREFIX = CoSky.COSKY + ".security"; + private boolean enabled = true; + private boolean enforceInitSuperUser = false; + private Jwt jwt; + + public SecurityProperties() { + jwt = new Jwt(); + } + + public boolean isEnforceInitSuperUser() { + return enforceInitSuperUser; + } + + public void setEnforceInitSuperUser(boolean enforceInitSuperUser) { + this.enforceInitSuperUser = enforceInitSuperUser; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Jwt getJwt() { + return jwt; + } + + public void setJwt(Jwt jwt) { + this.jwt = jwt; + } + + public static class Jwt { + private String algorithm = "HmacSHA256"; + private String signingKey; + private Duration accessTokenValidity = Duration.ofMinutes(10); + private Duration refreshTokenValidity = Duration.ofDays(7); + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public String getSigningKey() { + return signingKey; + } + + public void setSigningKey(String signingKey) { + this.signingKey = signingKey; + } + + public Duration getAccessTokenValidity() { + return accessTokenValidity; + } + + public void setAccessTokenValidity(Duration accessTokenValidity) { + this.accessTokenValidity = accessTokenValidity; + } + + public Duration getRefreshTokenValidity() { + return refreshTokenValidity; + } + + public void setRefreshTokenValidity(Duration refreshTokenValidity) { + this.refreshTokenValidity = refreshTokenValidity; + } + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/config/SecurityConfig.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/config/SecurityConfig.java new file mode 100644 index 00000000..792c3b4a --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/config/SecurityConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.config; + +import me.ahoo.cosid.snowflake.SnowflakeFriendlyId; +import me.ahoo.cosid.snowflake.SnowflakeId; +import me.ahoo.cosky.rest.security.AuthorizeHandlerInterceptor; +import me.ahoo.cosky.rest.security.JwtProvider; +import me.ahoo.cosky.rest.security.SecurityProperties; +import me.ahoo.cosky.rest.security.rbac.RBACService; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author ahoo wang + */ +@Configuration +@EnableConfigurationProperties(SecurityProperties.class) +public class SecurityConfig { + private final SecurityProperties securityProperties; + + public SecurityConfig(SecurityProperties securityProperties) { + this.securityProperties = securityProperties; + } + + @Bean + public JwtProvider jwtProvider(SnowflakeId snowflakeFriendlyId) { + return new JwtProvider(securityProperties.getJwt(), (SnowflakeFriendlyId) snowflakeFriendlyId); + } + + @Bean + public AuthorizeHandlerInterceptor authorizeHandlerInterceptor(RBACService rbacService) { + return new AuthorizeHandlerInterceptor(rbacService); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/config/SecurityInterceptorConfigurer.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/config/SecurityInterceptorConfigurer.java new file mode 100644 index 00000000..6b5130e6 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/config/SecurityInterceptorConfigurer.java @@ -0,0 +1,55 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.config; + +import me.ahoo.cosky.rest.security.AuthorizeHandlerInterceptor; +import me.ahoo.cosky.rest.security.ConditionalOnSecurityEnabled; +import me.ahoo.cosky.rest.support.RequestPathPrefix; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * @author ahoo wang + */ +@Configuration +@ConditionalOnSecurityEnabled +public class SecurityInterceptorConfigurer implements WebMvcConfigurer { + + private final AuthorizeHandlerInterceptor authorizeHandlerInterceptor; + + public SecurityInterceptorConfigurer(AuthorizeHandlerInterceptor authorizeHandlerInterceptor) { + this.authorizeHandlerInterceptor = authorizeHandlerInterceptor; + } + + /** + * Override this method to add Spring MVC interceptors for + * pre- and post-processing of controller invocation. + * + * @param registry + * @see InterceptorRegistry + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authorizeHandlerInterceptor) + .excludePathPatterns( + "/swagger-ui/**" + , "/swagger-resources/**" + , "/v3/api-docs" + , "/dashboard/**" + , RequestPathPrefix.AUTHENTICATE_PREFIX + "/**" + ) + .addPathPatterns("/**"); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/Action.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/Action.java new file mode 100644 index 00000000..391d2a1c --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/Action.java @@ -0,0 +1,82 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.rbac; + +import org.springframework.http.HttpMethod; + +/** + * @author ahoo wang + */ +public enum Action { + READ("r"), + WRITE("w"), + READ_WRITE("rw"); + + private final String value; + + Action(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static Action of(String value) { + switch (value) { + case "r": { + return READ; + } + case "w": { + return WRITE; + } + case "rw": { + return READ_WRITE; + } + default: + throw new IllegalStateException("Unexpected value: " + value); + } + } + + public static Action ofHttpMethod(String httpMethodStr) { + HttpMethod httpMethod = HttpMethod.resolve(httpMethodStr); + return ofHttpMethod(httpMethod); + } + + public static Action ofHttpMethod(HttpMethod httpMethod) { + switch (httpMethod) { + case GET: + case OPTIONS: + case TRACE: + case HEAD: + return READ; + case POST: + case PUT: + case DELETE: + case PATCH: + return WRITE; + default: + throw new IllegalStateException("Unexpected value: " + httpMethod); + } + } + + public boolean check(Action requestAction) { + if (READ_WRITE.value.equals(this.value)) { + return true; + } + return this.equals(requestAction); + } + + +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/NotFoundRoleException.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/NotFoundRoleException.java new file mode 100644 index 00000000..6b450043 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/NotFoundRoleException.java @@ -0,0 +1,32 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.rbac; + +import com.google.common.base.Strings; + +/** + * @author ahoo wang + */ +public class NotFoundRoleException extends SecurityException { + private final String roleName; + + public NotFoundRoleException(String roleName) { + super(Strings.lenientFormat("not found role:[%s]", roleName)); + this.roleName = roleName; + } + + public String getRoleName() { + return roleName; + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RBACService.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RBACService.java new file mode 100644 index 00000000..49daf4c7 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RBACService.java @@ -0,0 +1,137 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.rbac; + +import com.google.common.base.Strings; +import com.google.common.collect.Sets; +import io.lettuce.core.cluster.api.sync.RedisClusterCommands; +import me.ahoo.cosky.core.Namespaced; +import me.ahoo.cosky.core.redis.RedisConnectionFactory; +import me.ahoo.cosky.rest.dto.role.ResourceActionDto; +import me.ahoo.cosky.rest.security.JwtProvider; +import me.ahoo.cosky.rest.security.SecurityContext; +import me.ahoo.cosky.rest.security.rbac.annotation.AdminResource; +import me.ahoo.cosky.rest.security.user.User; +import me.ahoo.cosky.rest.support.RequestPathPrefix; +import org.springframework.stereotype.Service; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Set; + +/** + * @author ahoo wang + */ +@Service +public class RBACService { + + /** + * set + */ + public static final String ROLE_IDX = Namespaced.SYSTEM + ":role_idx"; + /** + * hash + */ + public static final String ROLE_RESOURCE_BIND = Namespaced.SYSTEM + ":role_resource_bind:%s"; + private final JwtProvider jwtProvider; + private final RedisClusterCommands redisCommands; + + public RBACService(JwtProvider jwtProvider, RedisConnectionFactory redisConnectionFactory) { + this.jwtProvider = jwtProvider; + this.redisCommands = redisConnectionFactory.getShareSyncCommands(); + } + + private String getRoleResourceBindKey(String roleName) { + return Strings.lenientFormat(ROLE_RESOURCE_BIND, roleName); + } + + public void saveRole(String roleName, Set resourceActionBind) { + redisCommands.sadd(ROLE_IDX, roleName); + String roleResourceBindKey = getRoleResourceBindKey(roleName); + redisCommands.del(roleResourceBindKey); + for (ResourceActionDto resourceAction : resourceActionBind) { + redisCommands.hset(roleResourceBindKey, resourceAction.getNamespace(), resourceAction.getAction()); + } + } + + public void removeRole(String roleName) { + redisCommands.srem(ROLE_IDX, roleName); + String roleResourceBindKey = getRoleResourceBindKey(roleName); + redisCommands.del(roleResourceBindKey); + } + + public Set getAllRole() { + Set roles = redisCommands.smembers(ROLE_IDX); + Set allRole = Sets.newHashSet(roles); + allRole.add(Role.ADMIN_ROLE); + return allRole; + } + + public Role getRole(String roleName) throws NotFoundRoleException { + String roleResourceBindKey = getRoleResourceBindKey(roleName); + Map roleResourceBindMap = redisCommands.hgetall(roleResourceBindKey); + if (roleResourceBindMap == null) { + throw new NotFoundRoleException(roleName); + } + Role role = new Role(); + role.setRoleName(roleName); + roleResourceBindMap.forEach((namespace, action) -> { + ResourceAction resourceAction = new ResourceAction(namespace, Action.of(action)); + role.getResourceActionBind().put(namespace, resourceAction); + }); + return role; + } + + /** + * 权限控制 + */ + public boolean authorize(String accessToken, HttpServletRequest request, HandlerMethod handlerMethod) { + + User user = jwtProvider.authorize(accessToken); + SecurityContext.setUser(user); + if (User.SUPER_USER.equals(user.getUsername())) { + return true; + } + + String requestUrl = request.getRequestURI(); + if (RequestPathPrefix.NAMESPACES_PREFIX.equals(requestUrl)) { + return true; + } + + boolean isAdminResource = handlerMethod.hasMethodAnnotation(AdminResource.class); + if (isAdminResource && !user.isAdmin()) { + return true; + } + + String namespace = requestUrl.substring(RequestPathPrefix.NAMESPACES_PREFIX.length() + 1); + int splitIdx = namespace.indexOf("/"); + if (splitIdx > 0) { + namespace = namespace.substring(0, splitIdx); + } + + ResourceAction requestAction = new ResourceAction(namespace, Action.ofHttpMethod(request.getMethod())); + return authorize(user, requestAction); + } + + /** + * 权限控制 + */ + public boolean authorize(User user, ResourceAction requestAction) { + return user.getRoleBind() + .stream() + .map(roleName -> getRole(roleName)) + .anyMatch(role -> role.check(requestAction)); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RequestAction.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RequestAction.java new file mode 100644 index 00000000..f3a1b8e3 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RequestAction.java @@ -0,0 +1,37 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.rbac; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author ahoo wang + */ +public class RequestAction { + private final HttpServletRequest request; + private final Action action; + + public RequestAction(HttpServletRequest request, Action action) { + this.request = request; + this.action = action; + } + + public HttpServletRequest getRequest() { + return request; + } + + public Action getAction() { + return action; + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/ResourceAction.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/ResourceAction.java new file mode 100644 index 00000000..cfa622be --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/ResourceAction.java @@ -0,0 +1,67 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.rbac; + +import com.google.common.base.Objects; +import me.ahoo.cosky.core.Namespaced; + +/** + * @author ahoo wang + */ +public class ResourceAction implements Namespaced { + private final String namespace; + private final Action action; + + public ResourceAction(String namespace, Action action) { + this.namespace = namespace; + this.action = action; + } + + @Override + public String getNamespace() { + return namespace; + } + + public Action getAction() { + return action; + } + + public boolean check(ResourceAction requestAction) { + if (!namespace.equals(requestAction.getNamespace())) { + return false; + } + return action.check(requestAction.getAction()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ResourceAction)) return false; + ResourceAction that = (ResourceAction) o; + return Objects.equal(namespace, that.namespace) && action == that.action; + } + + @Override + public int hashCode() { + return Objects.hashCode(namespace, action); + } + + @Override + public String toString() { + return "ResourceAction{" + + "namespace='" + namespace + '\'' + + ", action=" + action + + '}'; + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/Role.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/Role.java new file mode 100644 index 00000000..c887ec0d --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/Role.java @@ -0,0 +1,76 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.rbac; + +import com.google.common.base.Objects; + +import java.util.HashMap; +import java.util.Map; + +/** + * Role 1:n ResourceAction + * + * @author ahoo wang + */ +public class Role { + public static final String ADMIN_ROLE = "admin"; + + public static final Role ADMIN; + + static { + ADMIN = new Role(); + ADMIN.setRoleName(ADMIN_ROLE); + } + + private String roleName; + + private Map resourceActionBind = new HashMap<>(); + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + public Map getResourceActionBind() { + return resourceActionBind; + } + + public void setResourceActionBind(Map resourceActionBind) { + this.resourceActionBind = resourceActionBind; + } + + public boolean check(ResourceAction requestAction) { + ResourceAction resourceAction = resourceActionBind.get(requestAction.getNamespace()); + if (resourceAction == null) { + return false; + } + return resourceAction.check(requestAction); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Role)) return false; + Role role = (Role) o; + return Objects.equal(roleName, role.roleName); + } + + @Override + public int hashCode() { + return Objects.hashCode(roleName); + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/annotation/AdminResource.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/annotation/AdminResource.java new file mode 100644 index 00000000..2f9c77b4 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/annotation/AdminResource.java @@ -0,0 +1,27 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.rbac.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author ahoo wang + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AdminResource { +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/User.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/User.java new file mode 100644 index 00000000..1c227dfa --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/User.java @@ -0,0 +1,57 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.user; + +import me.ahoo.cosky.core.CoSky; +import me.ahoo.cosky.rest.security.rbac.Role; + +import java.util.Set; + +/** + * @author ahoo wang + */ +public class User { + public static String SUPER_USER = CoSky.COSKY; + + private String username; + private Set roleBind; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Set getRoleBind() { + return roleBind; + } + + public void setRoleBind(Set roleBind) { + this.roleBind = roleBind; + } + + public boolean isAdmin() { + return SUPER_USER.equals(username) || roleBind.contains(Role.ADMIN_ROLE); + } + + @Override + public String toString() { + return "User{" + + "username='" + username + '\'' + + ", roleBind=" + roleBind + + '}'; + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/UserService.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/UserService.java new file mode 100644 index 00000000..c2035e27 --- /dev/null +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/UserService.java @@ -0,0 +1,156 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ahoo.cosky.rest.security.user; + +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.hash.Hashing; +import io.lettuce.core.SetArgs; +import io.lettuce.core.cluster.api.sync.RedisClusterCommands; +import lombok.extern.slf4j.Slf4j; +import me.ahoo.cosky.core.Namespaced; +import me.ahoo.cosky.core.redis.RedisConnectionFactory; +import me.ahoo.cosky.rest.dto.user.LoginResponse; +import me.ahoo.cosky.rest.security.JwtProvider; +import net.bytebuddy.utility.RandomString; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author ahoo wang + */ +@Slf4j +@Service +public class UserService { + + public static final String USER_SAVE = "user_save.lua"; + public static final String USER_REMOVE = "user_remove.lua"; + public static final String USER_CHANGE_PWD = "user_change_pwd.lua"; + public static final String USER_LOGIN = "user_login.lua"; + + public static final String USER_IDX = Namespaced.SYSTEM + ":user_idx"; + public static final String USER_ROLE_BIND = Namespaced.SYSTEM + ":user_role_bind:%s"; + public static final String USER_LOGIN_LOCK = Namespaced.SYSTEM + ":login_lock:%s"; + private final JwtProvider jwtProvider; + private final RedisClusterCommands redisCommands; + + public UserService(JwtProvider jwtProvider, RedisConnectionFactory redisConnectionFactory) { + this.jwtProvider = jwtProvider; + this.redisCommands = redisConnectionFactory.getShareSyncCommands(); + } + + private static String getUserRoleBindKey(String username) { + return Strings.lenientFormat(USER_ROLE_BIND, username); + } + + public boolean initRoot(boolean enforce) { + + if (enforce) { + removeUser(User.SUPER_USER); + } + + final String coskyPwd = RandomString.make(10); + if (addUser(User.SUPER_USER, coskyPwd)) { + printSuperUserPwd(coskyPwd); + return true; + } + return false; + } + + private void printSuperUserPwd(String coskyPwd) { + System.out.println(Strings.lenientFormat("-------- CoSky - init super user:[%s] password:[%s] --------", User.SUPER_USER, coskyPwd)); + } + + public List query() { + return redisCommands.hkeys(USER_IDX).stream().map(username -> { + Set roleBind = getRoleBind(username); + User user = new User(); + user.setUsername(username); + user.setRoleBind(roleBind); + return user; + }).collect(Collectors.toList()); + } + + public boolean existsUser(String username) { + return redisCommands.hexists(USER_IDX, username); + } + + public boolean addUser(String username, String pwd) { + return redisCommands.hsetnx(USER_IDX, username, encodePwd(pwd)); + } + + public boolean removeUser(String username) { + return redisCommands.hdel(USER_IDX, username) > 0; + } + + public void bindRole(String username, Set roleBind) { + String userRoleBindKey = getUserRoleBindKey(username); + for (String role : roleBind) { + redisCommands.sadd(userRoleBindKey, role); + } + } + + public Set getRoleBind(String username) { + String userRoleBindKey = getUserRoleBindKey(username); + return redisCommands.smembers(userRoleBindKey); + } + + public boolean changePwd(String username, String oldPwd, String newPwd) { + oldPwd = encodePwd(oldPwd); + newPwd = encodePwd(newPwd); + String prePwd = redisCommands.hget(USER_IDX, username); + Preconditions.checkNotNull(prePwd, Strings.lenientFormat("username:[%s] not exists.", username)); + Preconditions.checkArgument(prePwd.equals(oldPwd), Strings.lenientFormat("username:[%s] - old password is incorrect.", username)); + return redisCommands.hset(USER_IDX, username, newPwd); + } + + public static final int MAX_LOGIN_ERROR_TIMES = 5; + public static final long LOGIN_LOCK_EXPIRE = Duration.ofHours(1).toMillis(); + + public LoginResponse login(String username, String pwd) { + String loginLockKey = Strings.lenientFormat(USER_LOGIN_LOCK, username); + + long tryCount = redisCommands.incr(loginLockKey); + redisCommands.pexpire(loginLockKey, LOGIN_LOCK_EXPIRE); + if (tryCount > MAX_LOGIN_ERROR_TIMES) { + + /** + * throw freeze + */ + } + + String realPwd = redisCommands.hget(USER_IDX, username); + Preconditions.checkNotNull(realPwd, Strings.lenientFormat("username:[%s] not exists.", username)); + String encodedPwd = encodePwd(pwd); + Preconditions.checkArgument(realPwd.equals(encodedPwd), Strings.lenientFormat("username:[%s] - password is incorrect.", username)); + Set roleBind = getRoleBind(username); + User user = new User(); + user.setUsername(username); + user.setRoleBind(roleBind); + return jwtProvider.generateToken(user); + } + + private String encodePwd(String pwd) { + return Hashing.sha256().hashString(pwd, Charsets.UTF_8).toString(); + } + + public void logout() { + + } +} diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/support/RequestPathPrefix.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/support/RequestPathPrefix.java index c0139ca5..d1bc2b1a 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/support/RequestPathPrefix.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/support/RequestPathPrefix.java @@ -13,11 +13,31 @@ package me.ahoo.cosky.rest.support; + /** * @author ahoo wang */ public interface RequestPathPrefix { String V1 = "/v1/"; + + //region Authenticate + /** + * /v1/auth + */ + String AUTHENTICATE_PREFIX = V1 + "authenticate"; + //endregion + //region user + /** + * /v1/users + */ + String USERS_PREFIX = V1 + "users"; + //endregion + //region role + /** + * /v1/roles + */ + String ROLES_PREFIX = V1 + "roles"; + //endregion //region namespaces /** * /v1/namespaces diff --git a/cosky-rest-api/src/main/resources/application.yaml b/cosky-rest-api/src/main/resources/application.yaml index da42bc61..d5a6907e 100644 --- a/cosky-rest-api/src/main/resources/application.yaml +++ b/cosky-rest-api/src/main/resources/application.yaml @@ -6,10 +6,26 @@ spring: weight: 18 web: resources: - static-locations: file:./cosky-dashboard/dist/dashboard + static-locations: file:./cosky-dashboard/dist/ servlet: multipart: max-file-size: 10MB mvc: async: request-timeout: 5s + +cosky: + security: + jwt: + signing-key: FyN0Igd80Gas3stTavArGKOYnS9uLWGW + enforce-init-super-user: true + enabled: true + +cosid: + namespace: ${spring.application.name} + snowflake: + enabled: true + machine: + distributor: + type: redis + diff --git a/cosky-spring-cloud-core/src/main/java/me/ahoo/cosky/spring/cloud/CoskyProperties.java b/cosky-spring-cloud-core/src/main/java/me/ahoo/cosky/spring/cloud/CoskyProperties.java index 54733012..c11ac57a 100644 --- a/cosky-spring-cloud-core/src/main/java/me/ahoo/cosky/spring/cloud/CoskyProperties.java +++ b/cosky-spring-cloud-core/src/main/java/me/ahoo/cosky/spring/cloud/CoskyProperties.java @@ -13,7 +13,7 @@ package me.ahoo.cosky.spring.cloud; -import me.ahoo.cosky.core.Consts; +import me.ahoo.cosky.core.CoSky; import me.ahoo.cosky.core.NamespacedProperties; import me.ahoo.cosky.core.redis.RedisConfig; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -24,7 +24,7 @@ */ @ConfigurationProperties(CoskyProperties.PREFIX) public class CoskyProperties extends NamespacedProperties { - public static final String PREFIX = "spring.cloud." + Consts.COSKY; + public static final String PREFIX = "spring.cloud." + CoSky.COSKY; private boolean enabled = true; diff --git a/spring-cloud-starter-cosky-config/src/main/java/me/ahoo/cosky/config/spring/cloud/CoskyPropertySourceLocator.java b/spring-cloud-starter-cosky-config/src/main/java/me/ahoo/cosky/config/spring/cloud/CoskyPropertySourceLocator.java index d04e43a3..4de07dbe 100644 --- a/spring-cloud-starter-cosky-config/src/main/java/me/ahoo/cosky/config/spring/cloud/CoskyPropertySourceLocator.java +++ b/spring-cloud-starter-cosky-config/src/main/java/me/ahoo/cosky/config/spring/cloud/CoskyPropertySourceLocator.java @@ -20,7 +20,7 @@ import lombok.var; import me.ahoo.cosky.config.Config; import me.ahoo.cosky.config.ConfigService; -import me.ahoo.cosky.core.Consts; +import me.ahoo.cosky.core.CoSky; import me.ahoo.cosky.core.NamespacedContext; import me.ahoo.cosky.core.util.Futures; import org.apache.logging.log4j.util.Strings; @@ -124,6 +124,6 @@ private Map getMapSource(String configId, List public static String getNameOfConfigId(String configId) { - return Consts.COSKY + ":" + configId; + return CoSky.COSKY + ":" + configId; } } From 4914f6bc0f9f43dd846fadf1cfb7e584d223b76f Mon Sep 17 00:00:00 2001 From: Ahoo Wang Date: Thu, 15 Jul 2021 00:45:20 +0800 Subject: [PATCH 2/4] add user-editor fix removeUser --- .../src/app/api/user/UserClient.ts | 7 ++- cosky-dashboard/src/app/app.module.ts | 6 ++- .../app/components/login/login.component.html | 6 +-- .../role-editor/role-editor.component.html | 1 + .../role-editor/role-editor.component.scss | 0 .../role-editor/role-editor.component.spec.ts | 25 ++++++++++ .../role/role-editor/role-editor.component.ts | 16 ++++++ .../app/components/role/role.component.html | 41 ++++++++++------ .../src/app/components/role/role.component.ts | 24 ++++++++- .../user-editor/user-editor.component.html | 28 +++++++++++ .../user-editor/user-editor.component.scss | 0 .../user-editor/user-editor.component.spec.ts | 25 ++++++++++ .../user/user-editor/user-editor.component.ts | 49 +++++++++++++++++++ .../app/components/user/user.component.html | 39 +++++++++------ .../src/app/components/user/user.component.ts | 25 ++++++++-- .../cosky/rest/controller/RoleController.java | 5 +- .../cosky/rest/controller/UserController.java | 2 +- .../cosky/rest/security/rbac/RBACService.java | 12 +++-- .../cosky/rest/security/user/UserService.java | 2 + 19 files changed, 264 insertions(+), 49 deletions(-) create mode 100644 cosky-dashboard/src/app/components/role/role-editor/role-editor.component.html create mode 100644 cosky-dashboard/src/app/components/role/role-editor/role-editor.component.scss create mode 100644 cosky-dashboard/src/app/components/role/role-editor/role-editor.component.spec.ts create mode 100644 cosky-dashboard/src/app/components/role/role-editor/role-editor.component.ts create mode 100644 cosky-dashboard/src/app/components/user/user-editor/user-editor.component.html create mode 100644 cosky-dashboard/src/app/components/user/user-editor/user-editor.component.scss create mode 100644 cosky-dashboard/src/app/components/user/user-editor/user-editor.component.spec.ts create mode 100644 cosky-dashboard/src/app/components/user/user-editor/user-editor.component.ts diff --git a/cosky-dashboard/src/app/api/user/UserClient.ts b/cosky-dashboard/src/app/api/user/UserClient.ts index 8227e11b..1888d542 100644 --- a/cosky-dashboard/src/app/api/user/UserClient.ts +++ b/cosky-dashboard/src/app/api/user/UserClient.ts @@ -33,9 +33,8 @@ export class UserClient { return this.httpClient.patch(apiUrl, {username, oldPassword, newPassword}); } - saveUser(username: string, password: string): Observable { - const apiUrl = `${this.apiPrefix}/${username}`; - return this.httpClient.put(apiUrl, {username, password}); + addUser(username: string, password: string): Observable { + return this.httpClient.post(this.apiPrefix, {username, password}); } removeUser(username: string): Observable { @@ -44,7 +43,7 @@ export class UserClient { } bindRole(username: string, roleBind: string[]): Observable { - const apiUrl = `${this.apiPrefix}/${username}`; + const apiUrl = `${this.apiPrefix}/${username}/role`; return this.httpClient.patch(apiUrl, roleBind); } diff --git a/cosky-dashboard/src/app/app.module.ts b/cosky-dashboard/src/app/app.module.ts index 421cde4c..c783d814 100644 --- a/cosky-dashboard/src/app/app.module.ts +++ b/cosky-dashboard/src/app/app.module.ts @@ -58,6 +58,8 @@ import {AuthInterceptor} from "./security/AuthInterceptor"; import { LoginComponent } from './components/login/login.component'; import { UserComponent } from './components/user/user.component'; import { RoleComponent } from './components/role/role.component'; +import { RoleEditorComponent } from './components/role/role-editor/role-editor.component'; +import { UserEditorComponent } from './components/user/user-editor/user-editor.component'; registerLocaleData(zh); @@ -81,7 +83,9 @@ export const httpInterceptorProviders = [ ConfigImporterComponent, LoginComponent, UserComponent, - RoleComponent + RoleComponent, + RoleEditorComponent, + UserEditorComponent ], imports: [ BrowserModule, diff --git a/cosky-dashboard/src/app/components/login/login.component.html b/cosky-dashboard/src/app/components/login/login.component.html index d6a25581..8127c50d 100644 --- a/cosky-dashboard/src/app/components/login/login.component.html +++ b/cosky-dashboard/src/app/components/login/login.component.html @@ -2,19 +2,19 @@

    - + - + - +
    diff --git a/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.html b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.html new file mode 100644 index 00000000..2403a23b --- /dev/null +++ b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.html @@ -0,0 +1 @@ +

    role-editor works!

    diff --git a/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.scss b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.spec.ts b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.spec.ts new file mode 100644 index 00000000..14ab3ab3 --- /dev/null +++ b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RoleEditorComponent } from './role-editor.component'; + +describe('RoleEditorComponent', () => { + let component: RoleEditorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ RoleEditorComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RoleEditorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.ts b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.ts new file mode 100644 index 00000000..f9fc5fe6 --- /dev/null +++ b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.ts @@ -0,0 +1,16 @@ +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; + +@Component({ + selector: 'app-role-editor', + templateUrl: './role-editor.component.html', + styleUrls: ['./role-editor.component.scss'] +}) +export class RoleEditorComponent implements OnInit { + @Input() role!: string|null; + @Output() afterSave: EventEmitter = new EventEmitter(); + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/cosky-dashboard/src/app/components/role/role.component.html b/cosky-dashboard/src/app/components/role/role.component.html index 2d3aeb98..8015044b 100644 --- a/cosky-dashboard/src/app/components/role/role.component.html +++ b/cosky-dashboard/src/app/components/role/role.component.html @@ -1,12 +1,12 @@ -
    -
    - @@ -17,20 +17,29 @@ {{role}} - + - + + + + diff --git a/cosky-dashboard/src/app/components/role/role.component.ts b/cosky-dashboard/src/app/components/role/role.component.ts index 3fb4dd64..ec5ff564 100644 --- a/cosky-dashboard/src/app/components/role/role.component.ts +++ b/cosky-dashboard/src/app/components/role/role.component.ts @@ -1,6 +1,9 @@ import {Component, OnInit} from '@angular/core'; import {UserDto} from "../../api/user/UserDto"; import {RoleClient} from "../../api/role/RoleClient"; +import {UserEditorComponent} from "../user/user-editor/user-editor.component"; +import {NzDrawerService} from "ng-zorro-antd/drawer"; +import {RoleEditorComponent} from "./role-editor/role-editor.component"; @Component({ selector: 'app-role', @@ -10,7 +13,7 @@ import {RoleClient} from "../../api/role/RoleClient"; export class RoleComponent implements OnInit { roles: string[] = []; - constructor(private roleClient: RoleClient) { + constructor(private roleClient: RoleClient, private drawerService: NzDrawerService) { } loadRoles() { @@ -33,7 +36,24 @@ export class RoleComponent implements OnInit { return 'admin' === role; } - showAddRole() { + openEditor(role: string | null) { + const title = role ? `Edit Role [${role}]` : 'Add Role'; + const drawerRef = this.drawerService.create({ + nzTitle: title, + nzWidth: '40%', + nzContent: RoleEditorComponent, + nzContentParams: { + role + } + }); + drawerRef.afterOpen.subscribe(() => { + drawerRef.getContentComponent()?.afterSave.subscribe(result => { + if (result) { + drawerRef.close('Operation successful'); + } + this.loadRoles(); + }); + }); } } diff --git a/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.html b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.html new file mode 100644 index 00000000..131698a8 --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.html @@ -0,0 +1,28 @@ +
    + + + + + + + + + + + + + + + + + + + + +
    diff --git a/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.scss b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.spec.ts b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.spec.ts new file mode 100644 index 00000000..156ba8d0 --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserEditorComponent } from './user-editor.component'; + +describe('UserEditorComponent', () => { + let component: UserEditorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserEditorComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserEditorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.ts b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.ts new file mode 100644 index 00000000..55c964b8 --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.ts @@ -0,0 +1,49 @@ +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {UserDto} from "../../../api/user/UserDto"; +import {FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {UserClient} from "../../../api/user/UserClient"; +import {RoleClient} from "../../../api/role/RoleClient"; + +@Component({ + selector: 'app-user-editor', + templateUrl: './user-editor.component.html', + styleUrls: ['./user-editor.component.scss'] +}) +export class UserEditorComponent implements OnInit { + @Input() user!: UserDto | null; + @Output() afterSave: EventEmitter = new EventEmitter(); + editorForm!: FormGroup; + username!: string; + password!: string; + roleBind!: string[]; + roles!: string[]; + + constructor(private userClient: UserClient, + private roleClient: RoleClient, + private formBuilder: FormBuilder) { + } + + ngOnInit(): void { + if (this.user) { + this.username = this.user.username; + this.roleBind = this.user.roleBind; + } + this.roleClient.getAllRole().subscribe(resp => { + this.roles = resp; + }) + const controlsConfig = { + username: [this.username, [Validators.required]], + password: [this.password, [Validators.required]] + }; + this.editorForm = this.formBuilder.group(controlsConfig); + } + + addUser() { + this.userClient.addUser(this.username, this.password).subscribe(resp => { + this.userClient.bindRole(this.username, this.roleBind).subscribe(bindResp => { + this.afterSave.emit(true); + }) + }); + } + +} diff --git a/cosky-dashboard/src/app/components/user/user.component.html b/cosky-dashboard/src/app/components/user/user.component.html index 0d651788..118e32a2 100644 --- a/cosky-dashboard/src/app/components/user/user.component.html +++ b/cosky-dashboard/src/app/components/user/user.component.html @@ -1,12 +1,12 @@ -
    -
    - @@ -20,18 +20,27 @@ {{user.username}} {{user.roleBind|json}} - + + + + diff --git a/cosky-dashboard/src/app/components/user/user.component.ts b/cosky-dashboard/src/app/components/user/user.component.ts index d972c7ce..a4bb7a47 100644 --- a/cosky-dashboard/src/app/components/user/user.component.ts +++ b/cosky-dashboard/src/app/components/user/user.component.ts @@ -1,6 +1,9 @@ import {Component, OnInit} from '@angular/core'; import {UserDto} from "../../api/user/UserDto"; import {UserClient} from "../../api/user/UserClient"; +import {NzDrawerService} from "ng-zorro-antd/drawer"; +import {ConfigImporterComponent} from "../config/config-importer/config-importer.component"; +import {UserEditorComponent} from "./user-editor/user-editor.component"; @Component({ selector: 'app-user', @@ -10,7 +13,7 @@ import {UserClient} from "../../api/user/UserClient"; export class UserComponent implements OnInit { users: UserDto[] = []; - constructor(private userClient: UserClient) { + constructor(private userClient: UserClient, private drawerService: NzDrawerService) { } loadUsers() { @@ -33,7 +36,23 @@ export class UserComponent implements OnInit { return 'cosky' === user.username; } - showAddUser() { - + openEditor(user: UserDto | null) { + const title = user ? `Edit User [${user.username}]` : 'Add User'; + const drawerRef = this.drawerService.create({ + nzTitle: title, + nzWidth: '40%', + nzContent: UserEditorComponent, + nzContentParams: { + user + } + }); + drawerRef.afterOpen.subscribe(() => { + drawerRef.getContentComponent()?.afterSave.subscribe(result => { + if (result) { + drawerRef.close('Operation successful'); + } + this.loadUsers(); + }); + }); } } diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/RoleController.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/RoleController.java index f5ce6626..dbb387d4 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/RoleController.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/RoleController.java @@ -28,6 +28,9 @@ @CrossOrigin("*") @RestController @RequestMapping(RequestPathPrefix.ROLES_PREFIX) +/** + * TODO @AdminResource + */ @AdminResource public class RoleController { private final RBACService rbacService; @@ -42,7 +45,7 @@ public Set getAllRole() { } @PutMapping("/{roleName}") - public void saveRole(@PathVariable String roleName,@RequestBody Set resourceActionBind) { + public void saveRole(@PathVariable String roleName, @RequestBody Set resourceActionBind) { rbacService.saveRole(roleName, resourceActionBind); } diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/UserController.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/UserController.java index d91bc266..04f8dfcb 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/UserController.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/controller/UserController.java @@ -48,7 +48,7 @@ public boolean changePwd(@PathVariable String username, @RequestBody ChangePwdRe } @AdminResource - @PostMapping("/") + @PostMapping public boolean addUser(@RequestBody AddUserRequest addUserRequest) { return userService.addUser(addUserRequest.getUsername(), addUserRequest.getPassword()); } diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RBACService.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RBACService.java index 49daf4c7..066a8f10 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RBACService.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/RBACService.java @@ -13,9 +13,11 @@ package me.ahoo.cosky.rest.security.rbac; +import com.google.common.base.Charsets; import com.google.common.base.Strings; import com.google.common.collect.Sets; import io.lettuce.core.cluster.api.sync.RedisClusterCommands; +import lombok.SneakyThrows; import me.ahoo.cosky.core.Namespaced; import me.ahoo.cosky.core.redis.RedisConnectionFactory; import me.ahoo.cosky.rest.dto.role.ResourceActionDto; @@ -28,6 +30,8 @@ import org.springframework.web.method.HandlerMethod; import javax.servlet.http.HttpServletRequest; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.util.Map; import java.util.Set; @@ -97,22 +101,24 @@ public Role getRole(String roleName) throws NotFoundRoleException { /** * 权限控制 */ + @SneakyThrows public boolean authorize(String accessToken, HttpServletRequest request, HandlerMethod handlerMethod) { User user = jwtProvider.authorize(accessToken); SecurityContext.setUser(user); - if (User.SUPER_USER.equals(user.getUsername())) { + if (User.SUPER_USER.equals(user.getUsername()) || user.isAdmin()) { return true; } String requestUrl = request.getRequestURI(); + requestUrl = URLDecoder.decode(requestUrl, Charsets.UTF_8.name()); if (RequestPathPrefix.NAMESPACES_PREFIX.equals(requestUrl)) { return true; } boolean isAdminResource = handlerMethod.hasMethodAnnotation(AdminResource.class); - if (isAdminResource && !user.isAdmin()) { - return true; + if (isAdminResource) { + return false; } String namespace = requestUrl.substring(RequestPathPrefix.NAMESPACES_PREFIX.length() + 1); diff --git a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/UserService.java b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/UserService.java index c2035e27..ebdda721 100644 --- a/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/UserService.java +++ b/cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/user/UserService.java @@ -96,6 +96,8 @@ public boolean addUser(String username, String pwd) { } public boolean removeUser(String username) { + String userRoleBindKey = getUserRoleBindKey(username); + redisCommands.del(userRoleBindKey); return redisCommands.hdel(USER_IDX, username) > 0; } From 0ee2d616736a985bd7a358138778a61ae19d0453 Mon Sep 17 00:00:00 2001 From: Ahoo Wang Date: Thu, 15 Jul 2021 21:07:46 +0800 Subject: [PATCH 3/4] add rbac impl add login-lock --- README.md | 63 ++++++++---- README.zh-CN.md | 62 +++++++---- cosky-dashboard/package.json | 2 +- .../src/app/api/role/RoleClient.ts | 16 ++- cosky-dashboard/src/app/api/role/RoleDto.ts | 17 ++++ .../src/app/api/user/UserClient.ts | 8 +- cosky-dashboard/src/app/app-routing.module.ts | 21 ++-- cosky-dashboard/src/app/app.component.html | 79 +------------- cosky-dashboard/src/app/app.component.scss | 96 ------------------ cosky-dashboard/src/app/app.component.spec.ts | 37 +++---- cosky-dashboard/src/app/app.component.ts | 2 - cosky-dashboard/src/app/app.module.ts | 8 +- .../authenticated.component.html | 93 +++++++++++++++++ .../authenticated.component.scss | 96 ++++++++++++++++++ .../authenticated.component.spec.ts | 25 +++++ .../authenticated/authenticated.component.ts | 49 +++++++++ .../app/components/login/login.component.html | 67 ++++++++---- .../app/components/login/login.component.scss | 5 +- .../app/components/login/login.component.ts | 11 +- .../namespace-selector.component.ts | 8 +- .../role-editor/role-editor.component.html | 72 ++++++++++++- .../role/role-editor/role-editor.component.ts | 57 ++++++++++- .../app/components/role/role.component.html | 7 +- .../src/app/components/role/role.component.ts | 17 ++-- .../service/instance-editor.service.ts | 3 +- .../user/user-add/user-add.component.html | 31 ++++++ .../user/user-add/user-add.component.scss | 0 .../user/user-add/user-add.component.spec.ts | 25 +++++ .../user/user-add/user-add.component.ts | 44 ++++++++ .../user-change-pwd.component.html | 17 ++++ .../user-change-pwd.component.scss | 0 .../user-change-pwd.component.spec.ts | 25 +++++ .../user-change-pwd.component.ts | 36 +++++++ .../user-editor/user-editor.component.html | 38 +++---- .../user/user-editor/user-editor.component.ts | 25 ++--- .../app/components/user/user.component.html | 12 ++- .../src/app/components/user/user.component.ts | 38 +++++-- cosky-dashboard/src/app/security/AuthGuard.ts | 35 ++++--- .../src/app/security/AuthInterceptor.ts | 47 +++++++-- .../src/app/security/SecurityService.ts | 60 +++++++---- cosky-dashboard/src/app/util/Clone.ts | 24 +++++ cosky-dashboard/src/index.html | 8 +- .../src/dist/config/application.yaml | 19 ++++ .../me/ahoo/cosky/rest/config/AppConfig.java | 3 - .../config/GlobalRestExceptionHandler.java | 21 ++++ .../rest/controller/NamespaceController.java | 12 ++- .../cosky/rest/controller/RoleController.java | 27 +++-- .../cosky/rest/controller/UserController.java | 15 ++- .../me/ahoo/cosky/rest/dto/role/RoleDto.java | 47 +++++++++ .../cosky/rest/dto/role/SaveRoleRequest.java | 49 +++++++++ .../security/AuthorizeHandlerInterceptor.java | 11 +- ...ption.java => CoSkySecurityException.java} | 12 +-- .../cosky/rest/security/SecurityContext.java | 13 ++- .../rest/security/TokenExpiredException.java | 92 +++++++++++++++++ .../config/SecurityInterceptorConfigurer.java | 3 +- .../cosky/rest/security/rbac/RBACService.java | 95 ++++++++++++----- .../ahoo/cosky/rest/security/rbac/Role.java | 10 +- .../rbac/annotation/OwnerResource.java | 27 +++++ .../cosky/rest/security/user/UserService.java | 55 ++++++---- .../src/main/resources/application.yaml | 12 ++- docs/dashboard-role-add.png | Bin 0 -> 239490 bytes docs/dashboard-role.png | Bin 0 -> 49176 bytes docs/dashboard-user-add.png | Bin 0 -> 46820 bytes docs/dashboard-user.png | Bin 0 -> 54786 bytes gradle.properties | 2 +- k8s/deployment/cosky-mirror.yml | 2 +- k8s/deployment/cosky-rest-api.yml | 2 +- k8s/docker/cosky-mirror/Dockerfile | 6 +- k8s/docker/rest-api-local/Dockerfile | 6 +- k8s/docker/rest-api/Dockerfile | 6 +- 70 files changed, 1440 insertions(+), 493 deletions(-) create mode 100644 cosky-dashboard/src/app/api/role/RoleDto.ts create mode 100644 cosky-dashboard/src/app/components/authenticated/authenticated.component.html create mode 100644 cosky-dashboard/src/app/components/authenticated/authenticated.component.scss create mode 100644 cosky-dashboard/src/app/components/authenticated/authenticated.component.spec.ts create mode 100644 cosky-dashboard/src/app/components/authenticated/authenticated.component.ts create mode 100644 cosky-dashboard/src/app/components/user/user-add/user-add.component.html create mode 100644 cosky-dashboard/src/app/components/user/user-add/user-add.component.scss create mode 100644 cosky-dashboard/src/app/components/user/user-add/user-add.component.spec.ts create mode 100644 cosky-dashboard/src/app/components/user/user-add/user-add.component.ts create mode 100644 cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.html create mode 100644 cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.scss create mode 100644 cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.spec.ts create mode 100644 cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.ts create mode 100644 cosky-dashboard/src/app/util/Clone.ts create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/role/RoleDto.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/dto/role/SaveRoleRequest.java rename cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/{SecurityException.java => CoSkySecurityException.java} (89%) create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/TokenExpiredException.java create mode 100644 cosky-rest-api/src/main/java/me/ahoo/cosky/rest/security/rbac/annotation/OwnerResource.java create mode 100644 docs/dashboard-role-add.png create mode 100644 docs/dashboard-role.png create mode 100644 docs/dashboard-user-add.png create mode 100644 docs/dashboard-user.png diff --git a/README.md b/README.md index 7b934a99..0d2b02da 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ between process cache and Redis. > Kotlin DSL ``` kotlin - val coskyVersion = "1.1.12"; + val coskyVersion = "1.2.0"; implementation("me.ahoo.cosky:spring-cloud-starter-cosky-config:${coskyVersion}") implementation("me.ahoo.cosky:spring-cloud-starter-cosky-discovery:${coskyVersion}") implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer:3.0.3") @@ -52,7 +52,7 @@ between process cache and Redis. 4.0.0 demo - 1.1.12 + 1.2.0 @@ -101,30 +101,21 @@ logging: #### Option 1:Download the executable file -> Download [cosky-rest-api-server](https://github.com/Ahoo-Wang/cosky/releases/download/1.1.12/cosky-rest-api-1.1.12.tar) +> Download [cosky-rest-api-server](https://github.com/Ahoo-Wang/cosky/releases/download/1.2.0/cosky-rest-api-1.2.0.tar) -> tar *cosky-rest-api-1.1.12.tar* +> tar *cosky-rest-api-1.2.0.tar* ```shell -cd cosky-rest-api-1.1.12 -# Working directory: cosky-rest-api-1.1.12 +cd cosky-rest-api-1.2.0 +# Working directory: cosky-rest-api-1.2.0 bin/cosky-rest-api --server.port=8080 --cosky.redis.uri=redis://localhost:6379 ``` #### Option 2:Run On Docker ```shell -docker pull ahoowang/cosky-rest-api:1.1.12 -docker run --name cosky-rest-api -d -p 8080:8080 --link redis -e COSKY_REDIS_URI=redis://redis:6379 ahoowang/cosky-rest-api:1.1.12 -``` - -##### MacBook Pro (M1) - -> Please use *ahoowang/cosky-rest-api:1.1.12-armv7* - -```shell -docker pull ahoowang/cosky-rest-api:1.1.12-armv7 -docker run --name cosky-rest-api -d -p 8080:8080 --link redis -e COSKY_REDIS_URI=redis://redis:6379 ahoowang/cosky-rest-api:1.1.12-armv7 +docker pull ahoowang/cosky-rest-api:1.2.0 +docker run --name cosky-rest-api -d -p 8080:8080 --link redis -e COSKY_REDIS_URI=redis://redis:6379 ahoowang/cosky-rest-api:1.2.0 ``` #### Option 3:Run On Kubernetes @@ -152,7 +143,7 @@ spec: value: standalone - name: COSKY_REDIS_URI value: redis://redis-uri:6379 - image: ahoowang/cosky-rest-api:1.1.12 + image: ahoowang/cosky-rest-api:1.2.0 name: cosky-rest-api ports: - containerPort: 8080 @@ -196,6 +187,34 @@ spec: ![dashboard-dashboard](./docs/dashboard-dashboard.png) +### Role-based access control(RBAC) + +- cosky: Reserved username, super user, with the highest authority. When the application is launched for the first time, the super user (cosky) password will be initialized and printed on the console. Don't worry if you forget your password, you can configure `enforce-init-super-user: true`, *CoSky* will help you reinitialize the password and print it on the console. + +```log +---------------- ****** CoSky - init super user:[cosky] password:[6TrmOux4Oj] ****** ---------------- +``` + +- admin: Reserved roles, super administrator roles, have all permissions, a user can be bound to multiple roles, and a role can be bound to multiple resource operation permissions. +- Permission control granularity is namespace, read and write operations + +#### Role Permissions + +![dashboard-role](./docs/dashboard-role.png) + +##### Add Role + +![dashboard-role-add](./docs/dashboard-role-add.png) + +#### User Management + +![dashboard-user](./docs/dashboard-user.png) + +##### Add User + +![dashboard-user-add](./docs/dashboard-user-add.png) + + #### Namespace ![dashboard-namespace](./docs/dashboard-namespace.png) @@ -286,12 +305,12 @@ spec: ``` shell gradle cosky-config:jmh # or -java -jar cosky-config/build/libs/cosky-config-1.1.12-jmh.jar -bm thrpt -t 25 -wi 1 -rf json -f 1 +java -jar cosky-config/build/libs/cosky-config-1.2.0-jmh.jar -bm thrpt -t 25 -wi 1 -rf json -f 1 ``` ``` # JMH version: 1.29 -# VM version: JDK 11.1.121, OpenJDK 64-Bit Server VM, 11.1.121+9-LTS +# VM version: JDK 11.2.01, OpenJDK 64-Bit Server VM, 11.2.01+9-LTS # VM invoker: /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java # VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/Users/ahoo/cosky/cosky-config/build/tmp/jmh -Duser.country=CN -Duser.language=zh -Duser.variant # Blackhole mode: full + dont-inline hint @@ -312,12 +331,12 @@ RedisConfigServiceBenchmark.setConfig thrpt 140461.112 ``` shell gradle cosky-discovery:jmh # or -java -jar cosky-discovery/build/libs/cosky-discovery-1.1.12-jmh.jar -bm thrpt -t 25 -wi 1 -rf json -f 1 +java -jar cosky-discovery/build/libs/cosky-discovery-1.2.0-jmh.jar -bm thrpt -t 25 -wi 1 -rf json -f 1 ``` ``` # JMH version: 1.29 -# VM version: JDK 11.1.121, OpenJDK 64-Bit Server VM, 11.1.121+9-LTS +# VM version: JDK 11.2.01, OpenJDK 64-Bit Server VM, 11.2.01+9-LTS # VM invoker: /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java # VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/Users/ahoo/cosky/cosky-discovery/build/tmp/jmh -Duser.country=CN -Duser.language=zh -Duser.variant # Blackhole mode: full + dont-inline hint diff --git a/README.zh-CN.md b/README.zh-CN.md index 0ba87962..2c25faf1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -33,7 +33,7 @@ > Kotlin DSL ``` kotlin - val coskyVersion = "1.1.12"; + val coskyVersion = "1.2.0"; implementation("me.ahoo.cosky:spring-cloud-starter-cosky-config:${coskyVersion}") implementation("me.ahoo.cosky:spring-cloud-starter-cosky-discovery:${coskyVersion}") implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer:3.0.3") @@ -51,7 +51,7 @@ 4.0.0 demo - 1.1.12 + 1.2.0 @@ -100,30 +100,21 @@ logging: #### 方式一:下载可执行文件 -> 下载 [rest-api-server](https://github.com/Ahoo-Wang/cosky/releases/download/1.1.12/cosky-rest-api-1.1.12.tar) +> 下载 [rest-api-server](https://github.com/Ahoo-Wang/cosky/releases/download/1.2.0/cosky-rest-api-1.2.0.tar) -> 解压 *cosky-rest-api-1.1.12.tar* +> 解压 *cosky-rest-api-1.2.0.tar* ```shell -cd cosky-rest-api-1.1.12 -# 工作目录: cosky-rest-api-1.1.12 +cd cosky-rest-api-1.2.0 +# 工作目录: cosky-rest-api-1.2.0 bin/cosky-rest-api --server.port=8080 --cosky.redis.uri=redis://localhost:6379 ``` #### 方式二:在 Docker 中运行 ```shell -docker pull ahoowang/cosky-rest-api:1.1.12 -docker run --name cosky-rest-api -d -p 8080:8080 --link redis -e COSKY_REDIS_URI=redis://redis:6379 ahoowang/cosky-rest-api:1.1.12 -``` - -##### MacBook Pro (M1) - -> 请使用 *ahoowang/cosky-rest-api:1.1.12-armv7* - -```shell -docker pull ahoowang/cosky-rest-api:1.1.12-armv7 -docker run --name cosky-rest-api -d -p 8080:8080 --link redis -e COSKY_REDIS_URI=redis://redis:6379 ahoowang/cosky-rest-api:1.1.12-armv7 +docker pull ahoowang/cosky-rest-api:1.2.0 +docker run --name cosky-rest-api -d -p 8080:8080 --link redis -e COSKY_REDIS_URI=redis://redis:6379 ahoowang/cosky-rest-api:1.2.0 ``` #### 方式三:在 Kubernetes 中运行 @@ -151,7 +142,7 @@ spec: value: standalone - name: COSKY_REDIS_URI value: redis://redis-uri:6379 - image: ahoowang/cosky-rest-api:1.1.12 + image: ahoowang/cosky-rest-api:1.2.0 name: cosky-rest-api ports: - containerPort: 8080 @@ -195,6 +186,33 @@ spec: ![dashboard-dashboard](./docs/dashboard-dashboard.png) +### 基于角色的访问控制(RBAC) + +- cosky: 保留用户名,超级用户,拥有最高权限。应用首次启动时会初始化超级用户(*cosky*)的密码,并打印在控制台。忘记密码也不用担心,可以通过配置 `enforce-init-super-user: true`,*CoSky* 会帮助你重新初始化密码并打印在控制台。 + +```log +---------------- ****** CoSky - init super user:[cosky] password:[6TrmOux4Oj] ****** ---------------- +``` + +- admin: 保留角色,超级管理员角色,拥有所有权限,一个用户可以绑定多个角色,一个角色可以绑定多个资源操作权限。 +- 权限控制粒度为命名空间,读写操作 + +#### 角色权限 + +![dashboard-role](./docs/dashboard-role.png) + +##### 添加角色 + +![dashboard-role-add](./docs/dashboard-role-add.png) + +#### 用户管理 + +![dashboard-user](./docs/dashboard-user.png) + +##### 添加用户 + +![dashboard-user-add](./docs/dashboard-user-add.png) + #### 命名空间管理 ![dashboard-namespace](./docs/dashboard-namespace.png) @@ -285,12 +303,12 @@ spec: ``` shell gradle cosky-config:jmh # or -java -jar cosky-config/build/libs/cosky-config-1.1.12-jmh.jar -bm thrpt -t 25 -wi 1 -rf json -f 1 +java -jar cosky-config/build/libs/cosky-config-1.2.0-jmh.jar -bm thrpt -t 25 -wi 1 -rf json -f 1 ``` ``` # JMH version: 1.29 -# VM version: JDK 11.1.121, OpenJDK 64-Bit Server VM, 11.1.121+9-LTS +# VM version: JDK 11.2.01, OpenJDK 64-Bit Server VM, 11.2.01+9-LTS # VM invoker: /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java # VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/Users/ahoo/cosky/config/build/tmp/jmh -Duser.country=CN -Duser.language=zh -Duser.variant # Blackhole mode: full + dont-inline hint @@ -311,12 +329,12 @@ RedisConfigServiceBenchmark.setConfig thrpt 140461.112 ``` shell gradle cosky-discovery:jmh # or -java -jar cosky-discovery/build/libs/cosky-discovery-1.1.12-jmh.jar -bm thrpt -t 25 -wi 1 -rf json -f 1 +java -jar cosky-discovery/build/libs/cosky-discovery-1.2.0-jmh.jar -bm thrpt -t 25 -wi 1 -rf json -f 1 ``` ``` # JMH version: 1.29 -# VM version: JDK 11.1.121, OpenJDK 64-Bit Server VM, 11.1.121+9-LTS +# VM version: JDK 11.2.01, OpenJDK 64-Bit Server VM, 11.2.01+9-LTS # VM invoker: /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java # VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/Users/ahoo/cosky/discovery/build/tmp/jmh -Duser.country=CN -Duser.language=zh -Duser.variant # Blackhole mode: full + dont-inline hint diff --git a/cosky-dashboard/package.json b/cosky-dashboard/package.json index fa8b46b2..0be20e0d 100644 --- a/cosky-dashboard/package.json +++ b/cosky-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "cosky-dashboard", - "version": "1.1.12", + "version": "1.2.0", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/cosky-dashboard/src/app/api/role/RoleClient.ts b/cosky-dashboard/src/app/api/role/RoleClient.ts index 23b78f0e..58d668a9 100644 --- a/cosky-dashboard/src/app/api/role/RoleClient.ts +++ b/cosky-dashboard/src/app/api/role/RoleClient.ts @@ -15,6 +15,7 @@ import {environment} from "../../../environments/environment"; import {Observable} from "rxjs"; import {HttpClient} from "@angular/common/http"; import {ResourceActionDto} from "./ResourceActionDto"; +import {RoleDto} from "./RoleDto"; @Injectable({providedIn: 'root'}) export class RoleClient { @@ -24,13 +25,18 @@ export class RoleClient { } - getAllRole(): Observable { - return this.httpClient.get(this.apiPrefix); + getAllRole(): Observable { + return this.httpClient.get(this.apiPrefix); } - saveRole(roleName: string, resourceActionBind: ResourceActionDto[]): Observable { - const apiUrl = `${this.apiPrefix}/${roleName}`; - return this.httpClient.patch(apiUrl, {roleName, resourceActionBind}); + getResourceBind(roleName: string):Observable{ + const apiUrl = `${this.apiPrefix}/${roleName}/bind`; + return this.httpClient.get(apiUrl); + } + + saveRole(roleName: string, desc: string, resourceActionBind: ResourceActionDto[]): Observable { + const apiUrl = `${this.apiPrefix}`; + return this.httpClient.put(apiUrl, {name: roleName, desc: desc, resourceActionBind}); } removeRole(roleName: string): Observable { diff --git a/cosky-dashboard/src/app/api/role/RoleDto.ts b/cosky-dashboard/src/app/api/role/RoleDto.ts new file mode 100644 index 00000000..7a3a42c2 --- /dev/null +++ b/cosky-dashboard/src/app/api/role/RoleDto.ts @@ -0,0 +1,17 @@ +/* + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface RoleDto { + name: string; + desc: string; +} diff --git a/cosky-dashboard/src/app/api/user/UserClient.ts b/cosky-dashboard/src/app/api/user/UserClient.ts index 1888d542..cde6938c 100644 --- a/cosky-dashboard/src/app/api/user/UserClient.ts +++ b/cosky-dashboard/src/app/api/user/UserClient.ts @@ -28,9 +28,9 @@ export class UserClient { return this.httpClient.get(this.apiPrefix); } - changePwd(username: string, oldPassword: string, newPassword: string): Observable { + changePwd(username: string, oldPassword: string, newPassword: string): Observable { const apiUrl = `${this.apiPrefix}/${username}/password`; - return this.httpClient.patch(apiUrl, {username, oldPassword, newPassword}); + return this.httpClient.patch(apiUrl, {username, oldPassword, newPassword}); } addUser(username: string, password: string): Observable { @@ -47,4 +47,8 @@ export class UserClient { return this.httpClient.patch(apiUrl, roleBind); } + unlock(username: string): Observable { + const apiUrl = `${this.apiPrefix}/${username}/lock`; + return this.httpClient.delete(apiUrl); + } } diff --git a/cosky-dashboard/src/app/app-routing.module.ts b/cosky-dashboard/src/app/app-routing.module.ts index ced087ed..efc2e3ac 100644 --- a/cosky-dashboard/src/app/app-routing.module.ts +++ b/cosky-dashboard/src/app/app-routing.module.ts @@ -21,17 +21,22 @@ import {UserComponent} from "./components/user/user.component"; import {AuthGuard} from "./security/AuthGuard"; import {RoleComponent} from "./components/role/role.component"; import {LoginComponent} from "./components/login/login.component"; +import {AuthenticatedComponent} from "./components/authenticated/authenticated.component"; const routes: Routes = [ - - {path: '', pathMatch: 'full', redirectTo: '/dashboard'}, {path: 'login', component: LoginComponent}, - {path: 'dashboard', canActivate: [AuthGuard],component: DashboardComponent}, - {path: 'namespace', canActivate: [AuthGuard],component: NamespaceComponent}, - {path: 'config', canActivate: [AuthGuard], component: ConfigComponent}, - {path: 'service', canActivate: [AuthGuard], component: ServiceComponent}, - {path: 'user', canActivate: [AuthGuard], component: UserComponent}, - {path: 'role', canActivate: [AuthGuard], component: RoleComponent} + { + path: '', canActivate: [AuthGuard], component: AuthenticatedComponent, + children: [ + {path: '', pathMatch: 'full', redirectTo: '/home'}, + {path: 'home', canActivate: [AuthGuard], component: DashboardComponent}, + {path: 'namespace', canActivate: [AuthGuard], component: NamespaceComponent}, + {path: 'config', canActivate: [AuthGuard], component: ConfigComponent}, + {path: 'service', canActivate: [AuthGuard], component: ServiceComponent}, + {path: 'user', canActivate: [AuthGuard], component: UserComponent}, + {path: 'role', canActivate: [AuthGuard], component: RoleComponent} + ] + } ]; @NgModule({ diff --git a/cosky-dashboard/src/app/app.component.html b/cosky-dashboard/src/app/app.component.html index fd8ad79d..0680b43f 100644 --- a/cosky-dashboard/src/app/app.component.html +++ b/cosky-dashboard/src/app/app.component.html @@ -1,78 +1 @@ - - - - - - - - - -
    - - - - -
    -
    - -
    - -
    -
    - - CoSky ©2021 - -
    -
    + diff --git a/cosky-dashboard/src/app/app.component.scss b/cosky-dashboard/src/app/app.component.scss index 9fb03bcd..e69de29b 100644 --- a/cosky-dashboard/src/app/app.component.scss +++ b/cosky-dashboard/src/app/app.component.scss @@ -1,96 +0,0 @@ -/*! - * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -:host { - display: flex; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.app-layout { - height: 100%; -} - -.menu-sidebar { - position: relative; - z-index: 10; - min-height: 100vh; - box-shadow: 2px 0 6px rgba(0,21,41,.35); -} - -.header-trigger { - height: 64px; - padding: 20px 24px; - font-size: 20px; - cursor: pointer; - transition: all .3s,padding 0s; -} - -.trigger:hover { - color: #1890ff; -} - -.sidebar-logo { - position: relative; - height: 64px; - overflow: hidden; - line-height: 64px; - background: #001529; - transition: all .3s; -} - -.sidebar-logo img { - display: inline-block; - height: 32px; - width: 32px; - vertical-align: middle; -} - -.sidebar-logo h1 { - display: inline-block; - margin: 0 0 0 20px; - color: #fff; - font-weight: 600; - font-size: 14px; - font-family: Avenir,Helvetica Neue,Arial,Helvetica,sans-serif; - vertical-align: middle; -} - -nz-header { - padding: 0; - width: 100%; - z-index: 2; -} - -.app-header { - position: relative; - height: 64px; - padding: 0; - background: #fff; - box-shadow: 0 1px 4px rgba(0,21,41,.08); -} - -nz-content { - margin: 24px; -} - -.inner-content { - //padding: 24px; - //background: #fff; - height: 100%; -} - -nz-footer { - text-align: center; -} diff --git a/cosky-dashboard/src/app/app.component.spec.ts b/cosky-dashboard/src/app/app.component.spec.ts index db7e70a0..cf34b6b1 100644 --- a/cosky-dashboard/src/app/app.component.spec.ts +++ b/cosky-dashboard/src/app/app.component.spec.ts @@ -11,38 +11,29 @@ * limitations under the License. */ -import {TestBed} from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import {RouterTestingModule} from '@angular/router/testing'; import {AppComponent} from './app.component'; +import {AuthenticatedComponent} from "./components/authenticated/authenticated.component"; describe('AppComponent', () => { + let component: AuthenticatedComponent; + let fixture: ComponentFixture; + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - RouterTestingModule - ], - declarations: [ - AppComponent - ], - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); + declarations: [ AuthenticatedComponent ] + }) + .compileComponents(); }); - it(`should have as title 'cosky-dashboard'`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('CoSky Dashboard'); + beforeEach(() => { + fixture = TestBed.createComponent(AuthenticatedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement; - expect(compiled.querySelector('.content span').textContent).toContain('cosky-dashboard app is running!'); + it('should create', () => { + expect(component).toBeTruthy(); }); }); diff --git a/cosky-dashboard/src/app/app.component.ts b/cosky-dashboard/src/app/app.component.ts index 7c849a57..ed351085 100644 --- a/cosky-dashboard/src/app/app.component.ts +++ b/cosky-dashboard/src/app/app.component.ts @@ -19,8 +19,6 @@ import {Component} from '@angular/core'; styleUrls: ['./app.component.scss'] }) export class AppComponent { - title = 'CoSky Dashboard'; - isCollapsed = false; constructor() { diff --git a/cosky-dashboard/src/app/app.module.ts b/cosky-dashboard/src/app/app.module.ts index c783d814..f85cc8e3 100644 --- a/cosky-dashboard/src/app/app.module.ts +++ b/cosky-dashboard/src/app/app.module.ts @@ -60,6 +60,9 @@ import { UserComponent } from './components/user/user.component'; import { RoleComponent } from './components/role/role.component'; import { RoleEditorComponent } from './components/role/role-editor/role-editor.component'; import { UserEditorComponent } from './components/user/user-editor/user-editor.component'; +import { AuthenticatedComponent } from './components/authenticated/authenticated.component'; +import { UserChangePwdComponent } from './components/user/user-change-pwd/user-change-pwd.component'; +import { UserAddComponent } from './components/user/user-add/user-add.component'; registerLocaleData(zh); @@ -85,7 +88,10 @@ export const httpInterceptorProviders = [ UserComponent, RoleComponent, RoleEditorComponent, - UserEditorComponent + UserEditorComponent, + AuthenticatedComponent, + UserChangePwdComponent, + UserAddComponent ], imports: [ BrowserModule, diff --git a/cosky-dashboard/src/app/components/authenticated/authenticated.component.html b/cosky-dashboard/src/app/components/authenticated/authenticated.component.html new file mode 100644 index 00000000..25c6e74a --- /dev/null +++ b/cosky-dashboard/src/app/components/authenticated/authenticated.component.html @@ -0,0 +1,93 @@ + + + + + + + + + +
    + + + + +
    + + + {{currentUser.sub}} + + + +
      +
    • Change Password
    • +
    • +
    • + Sign out
    • +
    +
    +
    +
    +
    + +
    + +
    +
    + + CoSky ©2021 + +
    +
    diff --git a/cosky-dashboard/src/app/components/authenticated/authenticated.component.scss b/cosky-dashboard/src/app/components/authenticated/authenticated.component.scss new file mode 100644 index 00000000..9fb03bcd --- /dev/null +++ b/cosky-dashboard/src/app/components/authenticated/authenticated.component.scss @@ -0,0 +1,96 @@ +/*! + * Copyright [2021-2021] [ahoo wang (https://github.com/Ahoo-Wang)]. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + display: flex; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.app-layout { + height: 100%; +} + +.menu-sidebar { + position: relative; + z-index: 10; + min-height: 100vh; + box-shadow: 2px 0 6px rgba(0,21,41,.35); +} + +.header-trigger { + height: 64px; + padding: 20px 24px; + font-size: 20px; + cursor: pointer; + transition: all .3s,padding 0s; +} + +.trigger:hover { + color: #1890ff; +} + +.sidebar-logo { + position: relative; + height: 64px; + overflow: hidden; + line-height: 64px; + background: #001529; + transition: all .3s; +} + +.sidebar-logo img { + display: inline-block; + height: 32px; + width: 32px; + vertical-align: middle; +} + +.sidebar-logo h1 { + display: inline-block; + margin: 0 0 0 20px; + color: #fff; + font-weight: 600; + font-size: 14px; + font-family: Avenir,Helvetica Neue,Arial,Helvetica,sans-serif; + vertical-align: middle; +} + +nz-header { + padding: 0; + width: 100%; + z-index: 2; +} + +.app-header { + position: relative; + height: 64px; + padding: 0; + background: #fff; + box-shadow: 0 1px 4px rgba(0,21,41,.08); +} + +nz-content { + margin: 24px; +} + +.inner-content { + //padding: 24px; + //background: #fff; + height: 100%; +} + +nz-footer { + text-align: center; +} diff --git a/cosky-dashboard/src/app/components/authenticated/authenticated.component.spec.ts b/cosky-dashboard/src/app/components/authenticated/authenticated.component.spec.ts new file mode 100644 index 00000000..4e6d2330 --- /dev/null +++ b/cosky-dashboard/src/app/components/authenticated/authenticated.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AuthenticatedComponent } from './authenticated.component'; + +describe('AuthenticatedComponent', () => { + let component: AuthenticatedComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AuthenticatedComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AuthenticatedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/cosky-dashboard/src/app/components/authenticated/authenticated.component.ts b/cosky-dashboard/src/app/components/authenticated/authenticated.component.ts new file mode 100644 index 00000000..873938d7 --- /dev/null +++ b/cosky-dashboard/src/app/components/authenticated/authenticated.component.ts @@ -0,0 +1,49 @@ +import {Component, OnInit} from '@angular/core'; +import {SecurityService} from "../../security/SecurityService"; +import {TokenPayload} from "../../api/authenticate/TokenPayload"; +import {NzDrawerService} from "ng-zorro-antd/drawer"; +import {UserChangePwdComponent} from "../user/user-change-pwd/user-change-pwd.component"; +import {NzMessageService} from "ng-zorro-antd/message"; + +@Component({ + selector: 'app-authenticated', + templateUrl: './authenticated.component.html', + styleUrls: ['./authenticated.component.scss'] +}) +export class AuthenticatedComponent implements OnInit { + title = 'CoSky Dashboard'; + isCollapsed = false; + currentUser: TokenPayload; + + constructor(private securityService: SecurityService + , private messageService: NzMessageService + , private drawerService: NzDrawerService) { + this.currentUser = this.securityService.getCurrentUser(); + } + + ngOnInit(): void { + + } + + + signOut() { + this.securityService.signOut(); + } + + + openChangePwd() { + const drawerRef = this.drawerService.create({ + nzTitle: `Change User:[${this.currentUser.sub}] Password`, + nzWidth: '30%', + nzContent: UserChangePwdComponent + }); + drawerRef.afterOpen.subscribe(() => { + drawerRef.getContentComponent()?.afterChange.subscribe(result => { + if (result) { + drawerRef.close('Operation successful'); + } + this.messageService.success("Password reset complete!") + }); + }); + } +} diff --git a/cosky-dashboard/src/app/components/login/login.component.html b/cosky-dashboard/src/app/components/login/login.component.html index 8127c50d..7a03236f 100644 --- a/cosky-dashboard/src/app/components/login/login.component.html +++ b/cosky-dashboard/src/app/components/login/login.component.html @@ -1,20 +1,47 @@ - - + + + + + + + + + + CoSky + ©2021 + + diff --git a/cosky-dashboard/src/app/components/login/login.component.scss b/cosky-dashboard/src/app/components/login/login.component.scss index 413653dc..07217813 100644 --- a/cosky-dashboard/src/app/components/login/login.component.scss +++ b/cosky-dashboard/src/app/components/login/login.component.scss @@ -1,4 +1,5 @@ .login-card{ - max-width: 500px; - margin: 200px auto; + width: 500px; + margin: 100px auto; + //float: right; } diff --git a/cosky-dashboard/src/app/components/login/login.component.ts b/cosky-dashboard/src/app/components/login/login.component.ts index abefbff6..e7aa24ed 100644 --- a/cosky-dashboard/src/app/components/login/login.component.ts +++ b/cosky-dashboard/src/app/components/login/login.component.ts @@ -1,6 +1,7 @@ import {Component, OnInit} from '@angular/core'; -import {SecurityService} from "../../security/SecurityService"; +import {HOME_PATH, SecurityService} from "../../security/SecurityService"; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {Router} from "@angular/router"; @Component({ selector: 'app-login', @@ -12,11 +13,15 @@ export class LoginComponent implements OnInit { username!: string; password!: string; - constructor(private securityService: SecurityService, - private formBuilder: FormBuilder) { + constructor(private securityService: SecurityService + , private router: Router + , private formBuilder: FormBuilder) { } ngOnInit(): void { + if (this.securityService.authenticated()) { + this.router.navigate([HOME_PATH]) + } const controlsConfig = { username: [this.username, [Validators.required]], password: [this.password, [Validators.required]] diff --git a/cosky-dashboard/src/app/components/namespace/namespace-selector/namespace-selector.component.ts b/cosky-dashboard/src/app/components/namespace/namespace-selector/namespace-selector.component.ts index 72aa2db6..a9bc7bcb 100644 --- a/cosky-dashboard/src/app/components/namespace/namespace-selector/namespace-selector.component.ts +++ b/cosky-dashboard/src/app/components/namespace/namespace-selector/namespace-selector.component.ts @@ -32,7 +32,13 @@ export class NamespaceSelectorComponent implements OnInit { this.currentNamespace = this.namespaceContext.getCurrent(); this.namespaceClient.getNamespaces().subscribe(namespaces => { this.namespaces = namespaces; - if (!this.currentNamespace && namespaces.length > 0) { + + if (namespaces.length === 0) { + return; + } + + if (!this.currentNamespace + || namespaces.indexOf(this.currentNamespace) < 0) { this.currentNamespace = namespaces[0]; this.onNamespaceSelected(); } diff --git a/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.html b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.html index 2403a23b..e424692e 100644 --- a/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.html +++ b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.html @@ -1 +1,71 @@ -

    role-editor works!

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + Namespace + Action + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.ts b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.ts index f9fc5fe6..59d23d0b 100644 --- a/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.ts +++ b/cosky-dashboard/src/app/components/role/role-editor/role-editor.component.ts @@ -1,4 +1,9 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {ResourceActionDto} from "../../../api/role/ResourceActionDto"; +import {NamespaceClient} from "../../../api/namespace/NamespaceClient"; +import {RoleClient} from "../../../api/role/RoleClient"; +import {RoleDto} from "../../../api/role/RoleDto"; +import {FormBuilder, FormGroup, Validators} from "@angular/forms"; @Component({ selector: 'app-role-editor', @@ -6,11 +11,59 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; styleUrls: ['./role-editor.component.scss'] }) export class RoleEditorComponent implements OnInit { - @Input() role!: string|null; + @Input() role!: RoleDto | null; @Output() afterSave: EventEmitter = new EventEmitter(); - constructor() { } + + resourceActionBind: ResourceActionDto[] = []; + namespaces: string[] = []; + roleName!: string; + desc!: string; + editorForm!: FormGroup; + isAdd!: boolean; + + constructor(private namespaceClient: NamespaceClient, private roleClient: RoleClient, + private formBuilder: FormBuilder) { + + } + + loadRole() { + if (!this.role) { + this.isAdd = true; + return; + } + this.isAdd = false; + this.roleName = this.role.name; + this.desc = this.role.desc; + this.roleClient.getResourceBind(this.roleName).subscribe(resp => { + this.resourceActionBind = resp; + }) + } ngOnInit(): void { + this.loadRole(); + const controlsConfig = { + roleName: [this.roleName, [Validators.required]], + desc: [this.desc, [Validators.required]] + }; + if (!this.isAdd) { + controlsConfig.roleName = [this.roleName]; + } + this.editorForm = this.formBuilder.group(controlsConfig); + this.namespaceClient.getNamespaces().subscribe(resp => this.namespaces = resp); } + + addResourceAction() { + this.resourceActionBind = [...this.resourceActionBind, {namespace: '', action: 'r'}] + } + + removeResourceAction(resourceAction: ResourceActionDto) { + this.resourceActionBind = this.resourceActionBind.filter(resource => resource != resourceAction); + } + + saveRole() { + this.roleClient.saveRole(this.roleName, this.desc, this.resourceActionBind).subscribe(resp => { + this.afterSave.emit(true) + }) + } } diff --git a/cosky-dashboard/src/app/components/role/role.component.html b/cosky-dashboard/src/app/components/role/role.component.html index 8015044b..3a1cfaef 100644 --- a/cosky-dashboard/src/app/components/role/role.component.html +++ b/cosky-dashboard/src/app/components/role/role.component.html @@ -1,5 +1,5 @@
    -
    +
    @@ -11,13 +11,14 @@ Role Name + Desc Action - {{role}} - + {{role.name}} + {{role.desc}} + diff --git a/cosky-dashboard/src/app/components/user/user-add/user-add.component.scss b/cosky-dashboard/src/app/components/user/user-add/user-add.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/cosky-dashboard/src/app/components/user/user-add/user-add.component.spec.ts b/cosky-dashboard/src/app/components/user/user-add/user-add.component.spec.ts new file mode 100644 index 00000000..74a02a1f --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user-add/user-add.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserAddComponent } from './user-add.component'; + +describe('UserAddComponent', () => { + let component: UserAddComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserAddComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserAddComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/cosky-dashboard/src/app/components/user/user-add/user-add.component.ts b/cosky-dashboard/src/app/components/user/user-add/user-add.component.ts new file mode 100644 index 00000000..b5fb2471 --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user-add/user-add.component.ts @@ -0,0 +1,44 @@ +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {UserClient} from "../../../api/user/UserClient"; +import {RoleClient} from "../../../api/role/RoleClient"; +import {RoleDto} from "../../../api/role/RoleDto"; + +@Component({ + selector: 'app-user-add', + templateUrl: './user-add.component.html', + styleUrls: ['./user-add.component.scss'] +}) +export class UserAddComponent implements OnInit { + @Output() afterAdd: EventEmitter = new EventEmitter(); + addForm!: FormGroup; + username!: string; + password!: string; + roleBind!: string[]; + roles!: RoleDto[]; + + constructor(private userClient: UserClient, + private roleClient: RoleClient, + private formBuilder: FormBuilder) { + } + + ngOnInit(): void { + this.roleClient.getAllRole().subscribe(resp => { + this.roles = resp; + }) + const controlsConfig = { + username: [this.username, [Validators.required]], + password: [this.password, [Validators.required]], + roleBind: [this.roleBind, [Validators.required]] + }; + this.addForm = this.formBuilder.group(controlsConfig); + } + + addUser() { + this.userClient.addUser(this.username, this.password).subscribe(resp => { + this.userClient.bindRole(this.username, this.roleBind).subscribe(bindResp => { + this.afterAdd.emit(true); + }) + }); + } +} diff --git a/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.html b/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.html new file mode 100644 index 00000000..38eb79ba --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.html @@ -0,0 +1,17 @@ +
    + + + + + + + + + + + + + + + +
    diff --git a/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.scss b/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.spec.ts b/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.spec.ts new file mode 100644 index 00000000..bbb074f9 --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserChangePwdComponent } from './user-change-pwd.component'; + +describe('UserChangePwdComponent', () => { + let component: UserChangePwdComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserChangePwdComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserChangePwdComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.ts b/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.ts new file mode 100644 index 00000000..e7d7bde3 --- /dev/null +++ b/cosky-dashboard/src/app/components/user/user-change-pwd/user-change-pwd.component.ts @@ -0,0 +1,36 @@ +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {UserClient} from "../../../api/user/UserClient"; +import {SecurityService} from "../../../security/SecurityService"; + +@Component({ + selector: 'app-user-change-pwd', + templateUrl: './user-change-pwd.component.html', + styleUrls: ['./user-change-pwd.component.scss'] +}) +export class UserChangePwdComponent implements OnInit { + editorForm!: FormGroup; + oldPassword!: string; + newPassword!: string; + @Output() afterChange: EventEmitter = new EventEmitter(); + + constructor(private userClient: UserClient, + private securityService: SecurityService, + private formBuilder: FormBuilder) { + } + + ngOnInit(): void { + const controlsConfig = { + oldPassword: [this.oldPassword, [Validators.required]], + newPassword: [this.newPassword, [Validators.required]] + }; + this.editorForm = this.formBuilder.group(controlsConfig); + } + + changePwd() { + const username = this.securityService.getCurrentUser().sub; + this.userClient.changePwd(username, this.oldPassword, this.newPassword).subscribe(result => { + this.afterChange.emit(true); + }); + } +} diff --git a/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.html b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.html index 131698a8..524415ce 100644 --- a/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.html +++ b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.html @@ -1,28 +1,16 @@ -
    - - - - - + + + + + + - - - - - - - - - - - - - + diff --git a/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.ts b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.ts index 55c964b8..90d206da 100644 --- a/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.ts +++ b/cosky-dashboard/src/app/components/user/user-editor/user-editor.component.ts @@ -3,6 +3,7 @@ import {UserDto} from "../../../api/user/UserDto"; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; import {UserClient} from "../../../api/user/UserClient"; import {RoleClient} from "../../../api/role/RoleClient"; +import {RoleDto} from "../../../api/role/RoleDto"; @Component({ selector: 'app-user-editor', @@ -10,13 +11,10 @@ import {RoleClient} from "../../../api/role/RoleClient"; styleUrls: ['./user-editor.component.scss'] }) export class UserEditorComponent implements OnInit { - @Input() user!: UserDto | null; + @Input() user!: UserDto; @Output() afterSave: EventEmitter = new EventEmitter(); editorForm!: FormGroup; - username!: string; - password!: string; - roleBind!: string[]; - roles!: string[]; + roles!: RoleDto[]; constructor(private userClient: UserClient, private roleClient: RoleClient, @@ -24,26 +22,19 @@ export class UserEditorComponent implements OnInit { } ngOnInit(): void { - if (this.user) { - this.username = this.user.username; - this.roleBind = this.user.roleBind; - } this.roleClient.getAllRole().subscribe(resp => { this.roles = resp; }) const controlsConfig = { - username: [this.username, [Validators.required]], - password: [this.password, [Validators.required]] + roleBind: [this.user.roleBind, [Validators.required]] }; this.editorForm = this.formBuilder.group(controlsConfig); } - addUser() { - this.userClient.addUser(this.username, this.password).subscribe(resp => { - this.userClient.bindRole(this.username, this.roleBind).subscribe(bindResp => { - this.afterSave.emit(true); - }) - }); + bindRole() { + this.userClient.bindRole(this.user.username, this.user.roleBind).subscribe(bindResp => { + this.afterSave.emit(true); + }) } } diff --git a/cosky-dashboard/src/app/components/user/user.component.html b/cosky-dashboard/src/app/components/user/user.component.html index 118e32a2..854c173d 100644 --- a/cosky-dashboard/src/app/components/user/user.component.html +++ b/cosky-dashboard/src/app/components/user/user.component.html @@ -1,6 +1,7 @@
    -
    -
    @@ -33,6 +34,13 @@ > +