Browse Source

Read-only option in VC repository settings

pull/7483/head
ViacheslavKlimov 4 years ago
committed by Artem Babak
parent
commit
a53a50e99f
  1. 22
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  2. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettings.java
  3. 30
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettingsInfo.java
  4. 7
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java
  5. 182
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  6. 14
      ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts
  7. 3
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html
  8. 1
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts
  9. 1
      ui-ngx/src/app/shared/models/settings.models.ts
  10. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

22
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
@ -195,7 +196,6 @@ public class AdminController extends BaseController {
notes = "Get the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings")
@ResponseBody
public RepositorySettings getRepositorySettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
@ -213,7 +213,6 @@ public class AdminController extends BaseController {
notes = "Check whether the repository settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings/exists")
@ResponseBody
public Boolean repositorySettingsExists() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
@ -223,6 +222,23 @@ public class AdminController extends BaseController {
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings/info")
public RepositorySettingsInfo getRepositorySettingsInfo() throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
RepositorySettings repositorySettings = versionControlService.getVersionControlSettings(getTenantId());
if (repositorySettings != null) {
return RepositorySettingsInfo.builder()
.configured(true)
.readOnly(repositorySettings.isReadOnly())
.build();
} else {
return RepositorySettingsInfo.builder()
.configured(false)
.build();
}
}
@ApiOperation(value = "Creates or Updates the repository settings (saveRepositorySettings)",
notes = "Creates or Updates the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@ -274,7 +290,6 @@ public class AdminController extends BaseController {
notes = "Get the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings")
@ResponseBody
public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
@ -288,7 +303,6 @@ public class AdminController extends BaseController {
notes = "Check whether the auto commit settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings/exists")
@ResponseBody
public Boolean autoCommitSettingsExists() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);

3
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettings.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.common.data.sync.vc;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.io.Serializable;
@ -32,6 +31,7 @@ public class RepositorySettings implements Serializable {
private String privateKey;
private String privateKeyPassword;
private String defaultBranch;
private boolean readOnly;
public RepositorySettings() {
}
@ -45,5 +45,6 @@ public class RepositorySettings implements Serializable {
this.privateKey = settings.getPrivateKey();
this.privateKeyPassword = settings.getPrivateKeyPassword();
this.defaultBranch = settings.getDefaultBranch();
this.readOnly = settings.isReadOnly();
}
}

30
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettingsInfo.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* 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 org.thingsboard.server.common.data.sync.vc;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RepositorySettingsInfo {
private boolean configured;
private Boolean readOnly;
}

7
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java

@ -47,6 +47,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@ -219,12 +220,14 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
@Override
public void testRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString());
GitRepository.test(settings, repositoryDirectory.toFile());
Path testDirectory = Path.of(repositoriesFolder, "repo-test-" + UUID.randomUUID());
GitRepository.test(settings, testDirectory.toFile());
}
@Override
public void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
testRepository(tenantId, settings);
clearRepository(tenantId);
log.debug("[{}] Init tenant repository started.", tenantId);
Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString());

182
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java

@ -20,7 +20,9 @@ import com.google.common.collect.Ordering;
import com.google.common.collect.Streams;
import lombok.Data;
import lombok.Getter;
import org.thingsboard.server.common.data.StringUtils;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
@ -49,6 +51,7 @@ import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.transport.sshd.JGitKeyCache;
import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
@ -61,8 +64,8 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@ -70,6 +73,7 @@ import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.ArrayList;
@ -78,70 +82,67 @@ import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
public class GitRepository {
private final Git git;
private final AuthHandler authHandler;
@Getter
private final RepositorySettings settings;
private final CredentialsProvider credentialsProvider;
private final SshdSessionFactory sshSessionFactory;
@Getter
private final String directory;
private ObjectId headId;
private GitRepository(Git git, RepositorySettings settings, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory, String directory) {
private GitRepository(Git git, RepositorySettings settings, AuthHandler authHandler, String directory) {
this.git = git;
this.settings = settings;
this.credentialsProvider = credentialsProvider;
this.sshSessionFactory = sshSessionFactory;
this.authHandler = authHandler;
this.directory = directory;
}
public static GitRepository clone(RepositorySettings settings, File directory) throws GitAPIException {
CredentialsProvider credentialsProvider = null;
SshdSessionFactory sshSessionFactory = null;
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
}
CloneCommand cloneCommand = Git.cloneRepository()
.setURI(settings.getRepositoryUri())
.setDirectory(directory)
.setNoCheckout(true);
configureTransportCommand(cloneCommand, credentialsProvider, sshSessionFactory);
AuthHandler authHandler = AuthHandler.createFor(settings, directory);
authHandler.configureCommand(cloneCommand);
Git git = cloneCommand.call();
return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath());
return new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
}
public static GitRepository open(File directory, RepositorySettings settings) throws IOException {
Git git = Git.open(directory);
CredentialsProvider credentialsProvider = null;
SshdSessionFactory sshSessionFactory = null;
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
}
return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath());
AuthHandler authHandler = AuthHandler.createFor(settings, directory);
return new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
}
public static void test(RepositorySettings settings, File directory) throws GitAPIException {
CredentialsProvider credentialsProvider = null;
SshdSessionFactory sshSessionFactory = null;
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
public static void test(RepositorySettings settings, File directory) throws Exception {
AuthHandler authHandler = AuthHandler.createFor(settings, directory);
if (settings.isReadOnly()) {
LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri());
authHandler.configureCommand(lsRemoteCommand);
lsRemoteCommand.call();
} else {
Files.createDirectories(directory.toPath());
try {
Git git = Git.init().setDirectory(directory).call();
GitRepository repository = new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
repository.execute(repository.git.remoteAdd()
.setName("origin")
.setUri(new URIish(settings.getRepositoryUri())));
repository.push("", UUID.randomUUID().toString()); // trying to delete non-existing branch on remote repo
} finally {
try {
FileUtils.forceDelete(directory);
} catch (Exception ignored) {}
}
}
LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri());
configureTransportCommand(lsRemoteCommand, credentialsProvider, sshSessionFactory);
lsRemoteCommand.call();
}
public void fetch() throws GitAPIException {
@ -363,12 +364,12 @@ public class GitRepository {
private <C extends GitCommand<T>, T> T execute(C command) throws GitAPIException {
if (command instanceof TransportCommand) {
configureTransportCommand((TransportCommand) command, credentialsProvider, sshSessionFactory);
authHandler.configureCommand((TransportCommand) command);
}
return command.call();
}
private static Function<PageLink, Comparator<RevCommit>> revCommitComparatorFunction = pageLink -> {
private static final Function<PageLink, Comparator<RevCommit>> revCommitComparatorFunction = pageLink -> {
SortOrder sortOrder = pageLink.getSortOrder();
if (sortOrder != null
&& sortOrder.getProperty().equals("timestamp")
@ -405,59 +406,76 @@ public class GitRepository {
return new PageData<>(data, totalPages, totalElements, hasNext);
}
private static void configureTransportCommand(TransportCommand transportCommand, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory) {
if (credentialsProvider != null) {
transportCommand.setCredentialsProvider(credentialsProvider);
}
if (sshSessionFactory != null) {
transportCommand.setTransportConfigCallback(transport -> {
if (transport instanceof SshTransport) {
SshTransport sshTransport = (SshTransport) transport;
sshTransport.setSshSessionFactory(sshSessionFactory);
}
});
@RequiredArgsConstructor
private static class AuthHandler {
private final CredentialsProvider credentialsProvider;
private final SshdSessionFactory sshSessionFactory;
protected static AuthHandler createFor(RepositorySettings settings, File directory) {
CredentialsProvider credentialsProvider = null;
SshdSessionFactory sshSessionFactory = null;
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
}
return new AuthHandler(credentialsProvider, sshSessionFactory);
}
}
private static CredentialsProvider newCredentialsProvider(String username, String password) {
return new UsernamePasswordCredentialsProvider(username, password == null ? "" : password);
}
protected void configureCommand(TransportCommand command) {
if (credentialsProvider != null) {
command.setCredentialsProvider(credentialsProvider);
}
if (sshSessionFactory != null) {
command.setTransportConfigCallback(transport -> {
if (transport instanceof SshTransport) {
SshTransport sshTransport = (SshTransport) transport;
sshTransport.setSshSessionFactory(sshSessionFactory);
}
});
}
}
private static SshdSessionFactory newSshdSessionFactory(String privateKey, String password, File directory) {
SshdSessionFactory sshSessionFactory = null;
if (StringUtils.isNotBlank(privateKey)) {
Iterable<KeyPair> keyPairs = loadKeyPairs(privateKey, password);
sshSessionFactory = new SshdSessionFactoryBuilder()
.setPreferredAuthentications("publickey")
.setDefaultKeysProvider(file -> keyPairs)
.setHomeDirectory(directory)
.setSshDirectory(directory)
.setServerKeyDatabase((file, file2) -> new ServerKeyDatabase() {
@Override
public List<PublicKey> lookup(String connectAddress, InetSocketAddress remoteAddress, Configuration config) {
return Collections.emptyList();
}
private static CredentialsProvider newCredentialsProvider(String username, String password) {
return new UsernamePasswordCredentialsProvider(username, password == null ? "" : password);
}
@Override
public boolean accept(String connectAddress, InetSocketAddress remoteAddress, PublicKey serverKey, Configuration config, CredentialsProvider provider) {
return true;
}
})
.build(new JGitKeyCache());
private static SshdSessionFactory newSshdSessionFactory(String privateKey, String password, File directory) {
SshdSessionFactory sshSessionFactory = null;
if (StringUtils.isNotBlank(privateKey)) {
Iterable<KeyPair> keyPairs = loadKeyPairs(privateKey, password);
sshSessionFactory = new SshdSessionFactoryBuilder()
.setPreferredAuthentications("publickey")
.setDefaultKeysProvider(file -> keyPairs)
.setHomeDirectory(directory)
.setSshDirectory(directory)
.setServerKeyDatabase((file, file2) -> new ServerKeyDatabase() {
@Override
public List<PublicKey> lookup(String connectAddress, InetSocketAddress remoteAddress, Configuration config) {
return Collections.emptyList();
}
@Override
public boolean accept(String connectAddress, InetSocketAddress remoteAddress, PublicKey serverKey, Configuration config, CredentialsProvider provider) {
return true;
}
})
.build(new JGitKeyCache());
}
return sshSessionFactory;
}
return sshSessionFactory;
}
private static Iterable<KeyPair> loadKeyPairs(String privateKeyContent, String password) {
Iterable<KeyPair> keyPairs = null;
try {
keyPairs = SecurityUtils.loadKeyPairIdentities(null,
null, new ByteArrayInputStream(privateKeyContent.getBytes()), (session, resourceKey, retryIndex) -> password);
} catch (Exception e) {}
if (keyPairs == null) {
throw new IllegalArgumentException("Failed to load ssh private key");
private static Iterable<KeyPair> loadKeyPairs(String privateKeyContent, String password) {
Iterable<KeyPair> keyPairs = null;
try {
keyPairs = SecurityUtils.loadKeyPairIdentities(null,
null, new ByteArrayInputStream(privateKeyContent.getBytes()), (session, resourceKey, retryIndex) -> password);
} catch (Exception e) {}
if (keyPairs == null) {
throw new IllegalArgumentException("Failed to load ssh private key");
}
return keyPairs;
}
return keyPairs;
}
private static class NoMergesAndCommitMessageFilter extends RevFilter {

14
ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.ts

@ -121,12 +121,18 @@ export class ComplexVersionCreateComponent extends PageComponent implements OnIn
}
this.versionCreateResultSubscription = this.versionCreateResult$.subscribe((result) => {
if (result.done && !result.added && !result.modified && !result.removed) {
this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(this.translate.instant('version-control.nothing-to-commit'));
let message;
if (!result.error) {
if (result.done && !result.added && !result.modified && !result.removed) {
message = this.translate.instant('version-control.nothing-to-commit')
} else {
message = this.translate.instant('version-control.version-create-result',
{added: result.added, modified: result.modified, removed: result.removed});
}
} else {
this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(result.error ? result.error : this.translate.instant('version-control.version-create-result',
{added: result.added, modified: result.modified, removed: result.removed}));
message = result.error;
}
this.resultMessage = this.sanitizer.bypassSecurityTrustHtml(message);
this.versionCreateResult = result;
this.versionCreateBranch = request.branch;
this.cd.detectChanges();

3
ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html

@ -41,6 +41,9 @@
<mat-label translate>admin.default-branch</mat-label>
<input matInput formControlName="defaultBranch">
</mat-form-field>
<mat-checkbox formControlName="readOnly" style="margin-bottom: 16px;">
{{ 'admin.repository-read-only' | translate }}
</mat-checkbox>
<fieldset [disabled]="isLoading$ | async" class="fields-group">
<legend class="group-title" translate>admin.authentication-settings</legend>
<mat-form-field fxFlex class="mat-block">

1
ui-ngx/src/app/modules/home/components/vc/repository-settings.component.ts

@ -74,6 +74,7 @@ export class RepositorySettingsComponent extends PageComponent implements OnInit
this.repositorySettingsForm = this.fb.group({
repositoryUri: [null, [Validators.required]],
defaultBranch: ['main', []],
readOnly: [false, []],
authMethod: [RepositoryAuthMethod.USERNAME_PASSWORD, [Validators.required]],
username: [null, []],
password: [null, []],

1
ui-ngx/src/app/shared/models/settings.models.ts

@ -411,6 +411,7 @@ export const repositoryAuthMethodTranslationMap = new Map<RepositoryAuthMethod,
export interface RepositorySettings {
repositoryUri: string;
defaultBranch: string;
readOnly: boolean;
authMethod: RepositoryAuthMethod;
username: string;
password: string;

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -328,6 +328,7 @@
"repository-url": "Repository URL",
"repository-url-required": "Repository URL is required.",
"default-branch": "Default branch name",
"repository-read-only": "Read-only",
"authentication-settings": "Authentication settings",
"auth-method": "Authentication method",
"auth-method-username-password": "Password / access token",

Loading…
Cancel
Save