Merge pull request #7483 from deaflynx/version-control-read-only
[3.4.2] Feature: Version control Repository settings with 'Read-only' flag
This commit is contained in:
commit
7ba76e8f59
@ -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);
|
||||
|
||||
@ -533,7 +533,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
|
||||
@Override
|
||||
public ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception {
|
||||
var repositorySettings = repositorySettingsService.get(user.getTenantId());
|
||||
if (repositorySettings == null) {
|
||||
if (repositorySettings == null || repositorySettings.isReadOnly()) {
|
||||
return Futures.immediateFuture(null);
|
||||
}
|
||||
var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId());
|
||||
@ -560,7 +560,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
|
||||
@Override
|
||||
public ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) throws Exception {
|
||||
var repositorySettings = repositorySettingsService.get(user.getTenantId());
|
||||
if (repositorySettings == null) {
|
||||
if (repositorySettings == null || repositorySettings.isReadOnly()) {
|
||||
return Futures.immediateFuture(null);
|
||||
}
|
||||
var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId());
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
|
||||
private static CredentialsProvider newCredentialsProvider(String username, String password) {
|
||||
return new UsernamePasswordCredentialsProvider(username, password == null ? "" : password);
|
||||
}
|
||||
|
||||
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());
|
||||
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);
|
||||
}
|
||||
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");
|
||||
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 CredentialsProvider newCredentialsProvider(String username, String password) {
|
||||
return new UsernamePasswordCredentialsProvider(username, password == null ? "" : password);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
///
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
|
||||
import { Observable } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {
|
||||
AdminSettings,
|
||||
@ -24,12 +24,12 @@ import {
|
||||
MailServerSettings,
|
||||
SecuritySettings,
|
||||
TestSmsRequest,
|
||||
UpdateMessage, AutoCommitSettings
|
||||
UpdateMessage,
|
||||
AutoCommitSettings,
|
||||
RepositorySettingsInfo
|
||||
} from '@shared/models/settings.models';
|
||||
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { AuthUser } from '@shared/models/user.model';
|
||||
import { Authority } from '@shared/models/authority.enum';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -97,6 +97,10 @@ export class AdminService {
|
||||
return this.http.post<void>('/api/admin/repositorySettings/checkAccess', repositorySettings, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public getRepositorySettingsInfo(config?: RequestConfig): Observable<RepositorySettingsInfo> {
|
||||
return this.http.get<RepositorySettingsInfo>('/api/admin/repositorySettings/info', defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public getAutoCommitSettings(config?: RequestConfig): Observable<AutoCommitSettings> {
|
||||
return this.http.get<AutoCommitSettings>(`/api/admin/autoCommitSettings`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
|
||||
<mat-card-content style="padding-top: 16px;">
|
||||
<form [formGroup]="autoCommitSettingsForm" #formDirective="ngForm" (ngSubmit)="save()">
|
||||
<fieldset class="fields-group" [disabled]="isLoading$ | async">
|
||||
<fieldset class="fields-group" [disabled]="(isLoading$ | async) || (isReadOnly | async)">
|
||||
<legend class="group-title" translate>admin.auto-commit-entities</legend>
|
||||
<div fxLayout="column">
|
||||
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
|
||||
@ -111,13 +111,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="tb-hint" *ngIf="isReadOnly | async" translate>version-control.auto-commit-settings-read-only-hint</div>
|
||||
<div fxLayout="row" fxLayoutAlign="end center" fxLayout.xs="column" fxLayoutAlign.xs="end" fxLayoutGap="16px">
|
||||
<button mat-raised-button color="warn" type="button" [fxShow]="settings !== null"
|
||||
[disabled]="(isLoading$ | async)" (click)="delete(formDirective)">
|
||||
[disabled]="(isLoading$ | async) || (isReadOnly | async)" (click)="delete(formDirective)">
|
||||
{{'action.delete' | translate}}
|
||||
</button>
|
||||
<span fxFlex></span>
|
||||
<button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || autoCommitSettingsForm.invalid || !autoCommitSettingsForm.dirty"
|
||||
<button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || (isReadOnly | async) || autoCommitSettingsForm.invalid || !autoCommitSettingsForm.dirty"
|
||||
type="submit">{{'action.save' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -23,8 +23,8 @@ import { AdminService } from '@core/http/admin.service';
|
||||
import { AutoCommitSettings, AutoVersionCreateConfig } from '@shared/models/settings.models';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { catchError, mergeMap } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
import { catchError, map, mergeMap } from 'rxjs/operators';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { EntityTypeVersionCreateConfig, exportableEntityTypes } from '@shared/models/vc.models';
|
||||
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
@ -41,6 +41,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
|
||||
|
||||
entityTypes = EntityType;
|
||||
|
||||
isReadOnly: Observable<boolean>;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private adminService: AdminService,
|
||||
private dialogService: DialogService,
|
||||
@ -71,6 +73,7 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
|
||||
this.autoCommitSettingsForm.setControl('entityTypes',
|
||||
this.prepareEntityTypesFormArray(settings), {emitEvent: false});
|
||||
});
|
||||
this.isReadOnly = this.adminService.getRepositorySettingsInfo().pipe(map(settings => settings.readOnly));
|
||||
}
|
||||
|
||||
entityTypesFormGroupArray(): FormGroup[] {
|
||||
|
||||
@ -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: string;
|
||||
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();
|
||||
|
||||
@ -34,14 +34,14 @@
|
||||
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center end">
|
||||
<button *ngIf="singleEntityMode" mat-stroked-button color="primary"
|
||||
#createVersionButton
|
||||
[disabled]="(isLoading$ | async)"
|
||||
[disabled]="(isLoading$ | async) || (isReadOnly | async)"
|
||||
(click)="toggleCreateVersion($event, createVersionButton)">
|
||||
<mat-icon>update</mat-icon>
|
||||
{{'version-control.create-version' | translate }}
|
||||
</button>
|
||||
<button *ngIf="!singleEntityMode" mat-stroked-button color="primary"
|
||||
#complexCreateVersionButton
|
||||
[disabled]="(isLoading$ | async)"
|
||||
[disabled]="(isLoading$ | async) || (isReadOnly | async)"
|
||||
(click)="toggleComplexCreateVersion($event, complexCreateVersionButton)">
|
||||
<mat-icon>update</mat-icon>
|
||||
{{'version-control.create-entities-version' | translate }}
|
||||
|
||||
@ -54,6 +54,7 @@ import { EntityVersionDiffComponent } from '@home/components/vc/entity-version-d
|
||||
import { ComplexVersionCreateComponent } from '@home/components/vc/complex-version-create.component';
|
||||
import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version-load.component';
|
||||
import { TbPopoverComponent } from '@shared/components/popover.component';
|
||||
import { AdminService } from "@core/http/admin.service";
|
||||
|
||||
@Component({
|
||||
selector: 'tb-entity-versions-table',
|
||||
@ -87,6 +88,8 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
|
||||
|
||||
viewsInited = false;
|
||||
|
||||
isReadOnly: Observable<boolean>;
|
||||
|
||||
private componentResize$: ResizeObserver;
|
||||
|
||||
@Input()
|
||||
@ -129,6 +132,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private entitiesVersionControlService: EntitiesVersionControlService,
|
||||
private adminService: AdminService,
|
||||
private popoverService: TbPopoverService,
|
||||
private renderer: Renderer2,
|
||||
private cd: ChangeDetectorRef,
|
||||
@ -150,6 +154,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
|
||||
}
|
||||
});
|
||||
this.componentResize$.observe(this.elementRef.nativeElement);
|
||||
this.isReadOnly = this.adminService.getRepositorySettingsInfo().pipe(map(settings => settings.readOnly));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@ -41,6 +41,9 @@
|
||||
<mat-label translate>admin.default-branch</mat-label>
|
||||
<input matInput formControlName="defaultBranch">
|
||||
</mat-form-field>
|
||||
<mat-checkbox formControlName="readOnly">
|
||||
{{ '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">
|
||||
@ -58,7 +61,7 @@
|
||||
autocomplete="new-username"/>
|
||||
</mat-form-field>
|
||||
<mat-checkbox *ngIf="showChangePassword" (change)="changePasswordChanged()"
|
||||
[(ngModel)]="changePassword" [ngModelOptions]="{ standalone: true }" style="padding-bottom: 16px;">
|
||||
[(ngModel)]="changePassword" [ngModelOptions]="{ standalone: true }">
|
||||
{{ 'admin.change-password-access-token' | translate }}
|
||||
</mat-checkbox>
|
||||
<mat-form-field class="mat-block" *ngIf="changePassword || !showChangePassword">
|
||||
@ -78,7 +81,7 @@
|
||||
(fileNameChanged)="repositorySettingsForm.get('privateKeyFileName').patchValue($event)">
|
||||
</tb-file-input>
|
||||
<mat-checkbox *ngIf="showChangePrivateKeyPassword" (change)="changePrivateKeyPasswordChanged()"
|
||||
[(ngModel)]="changePrivateKeyPassword" [ngModelOptions]="{ standalone: true }" style="padding-bottom: 16px;">
|
||||
[(ngModel)]="changePrivateKeyPassword" [ngModelOptions]="{ standalone: true }">
|
||||
{{ 'admin.change-passphrase' | translate }}
|
||||
</mat-checkbox>
|
||||
<mat-form-field class="mat-block" *ngIf="changePrivateKeyPassword || !showChangePrivateKeyPassword">
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
mat-card.repository-settings {
|
||||
margin: 8px;
|
||||
}
|
||||
mat-checkbox {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.fields-group {
|
||||
padding: 0 16px 8px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@ -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, []],
|
||||
|
||||
@ -419,6 +419,11 @@ export interface RepositorySettings {
|
||||
privateKeyPassword: string;
|
||||
}
|
||||
|
||||
export interface RepositorySettingsInfo {
|
||||
configured: boolean;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export interface AutoVersionCreateConfig extends VersionCreateConfig {
|
||||
branch: string;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
@ -3504,7 +3505,8 @@
|
||||
"sync-strategy-overwrite-hint": "Creates or updates selected entities in the repository. All other repository entities are <b>deleted</b>.",
|
||||
"device-credentials-conflict": "Failed to load the device with external id <b>{{entityId}}</b><br/>due to the same credentials are already present in the database for another device.<br/>Please consider disabling the <b>load credentials</b> setting in the restore form.",
|
||||
"missing-referenced-entity": "Failed to load the <b>{{sourceEntityTypeName}}</b> with external id <b>{{sourceEntityId}}</b><br/>because it references missing <b>{{targetEntityTypeName}}</b> with id <b>{{targetEntityId}}</b>.",
|
||||
"runtime-failed": "<b>Failed:</b> {{message}}"
|
||||
"runtime-failed": "<b>Failed:</b> {{message}}",
|
||||
"auto-commit-settings-read-only-hint": "Auto-commit feature doesn't work with enabled read-only option in Repository settings."
|
||||
},
|
||||
"widget": {
|
||||
"widget-library": "Widgets Library",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user