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:
Andrew Shvayka 2022-10-31 18:05:58 +02:00 committed by GitHub
commit 7ba76e8f59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 212 additions and 113 deletions

View File

@ -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.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings; 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.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation; 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) notes = "Get the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings") @GetMapping("/repositorySettings")
@ResponseBody
public RepositorySettings getRepositorySettings() throws ThingsboardException { public RepositorySettings getRepositorySettings() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); 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) notes = "Check whether the repository settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings/exists") @GetMapping("/repositorySettings/exists")
@ResponseBody
public Boolean repositorySettingsExists() throws ThingsboardException { public Boolean repositorySettingsExists() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); 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)", @ApiOperation(value = "Creates or Updates the repository settings (saveRepositorySettings)",
notes = "Creates or Updates the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Creates or Updates the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@ -274,7 +290,6 @@ public class AdminController extends BaseController {
notes = "Get the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Get the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings") @GetMapping("/autoCommitSettings")
@ResponseBody
public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException { public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); 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) notes = "Check whether the auto commit settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings/exists") @GetMapping("/autoCommitSettings/exists")
@ResponseBody
public Boolean autoCommitSettingsExists() throws ThingsboardException { public Boolean autoCommitSettingsExists() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);

View File

@ -533,7 +533,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override @Override
public ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception { public ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception {
var repositorySettings = repositorySettingsService.get(user.getTenantId()); var repositorySettings = repositorySettingsService.get(user.getTenantId());
if (repositorySettings == null) { if (repositorySettings == null || repositorySettings.isReadOnly()) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId()); var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId());
@ -560,7 +560,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override @Override
public ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) throws Exception { public ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) throws Exception {
var repositorySettings = repositorySettingsService.get(user.getTenantId()); var repositorySettings = repositorySettingsService.get(user.getTenantId());
if (repositorySettings == null) { if (repositorySettings == null || repositorySettings.isReadOnly()) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId()); var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId());

View File

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

View File

@ -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;
}

View File

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

View File

@ -20,7 +20,9 @@ import com.google.common.collect.Ordering;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import lombok.Data; import lombok.Data;
import lombok.Getter; 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.apache.sshd.common.util.security.SecurityUtils;
import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git; 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.FetchResult;
import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.SshTransport; import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.transport.sshd.JGitKeyCache; import org.eclipse.jgit.transport.sshd.JGitKeyCache;
import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; 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.PageLink;
import org.thingsboard.server.common.data.page.SortOrder; 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.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.RepositoryAuthMethod;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -70,6 +73,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.ArrayList; import java.util.ArrayList;
@ -78,70 +82,67 @@ import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class GitRepository { public class GitRepository {
private final Git git; private final Git git;
private final AuthHandler authHandler;
@Getter @Getter
private final RepositorySettings settings; private final RepositorySettings settings;
private final CredentialsProvider credentialsProvider;
private final SshdSessionFactory sshSessionFactory;
@Getter @Getter
private final String directory; private final String directory;
private ObjectId headId; 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.git = git;
this.settings = settings; this.settings = settings;
this.credentialsProvider = credentialsProvider; this.authHandler = authHandler;
this.sshSessionFactory = sshSessionFactory;
this.directory = directory; this.directory = directory;
} }
public static GitRepository clone(RepositorySettings settings, File directory) throws GitAPIException { 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() CloneCommand cloneCommand = Git.cloneRepository()
.setURI(settings.getRepositoryUri()) .setURI(settings.getRepositoryUri())
.setDirectory(directory) .setDirectory(directory)
.setNoCheckout(true); .setNoCheckout(true);
configureTransportCommand(cloneCommand, credentialsProvider, sshSessionFactory); AuthHandler authHandler = AuthHandler.createFor(settings, directory);
authHandler.configureCommand(cloneCommand);
Git git = cloneCommand.call(); 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 { public static GitRepository open(File directory, RepositorySettings settings) throws IOException {
Git git = Git.open(directory); Git git = Git.open(directory);
CredentialsProvider credentialsProvider = null; AuthHandler authHandler = AuthHandler.createFor(settings, directory);
SshdSessionFactory sshSessionFactory = null; return new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
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());
} }
public static void test(RepositorySettings settings, File directory) throws GitAPIException { public static void test(RepositorySettings settings, File directory) throws Exception {
CredentialsProvider credentialsProvider = null; AuthHandler authHandler = AuthHandler.createFor(settings, directory);
SshdSessionFactory sshSessionFactory = null; if (settings.isReadOnly()) {
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);
}
LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri()); LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri());
configureTransportCommand(lsRemoteCommand, credentialsProvider, sshSessionFactory); authHandler.configureCommand(lsRemoteCommand);
lsRemoteCommand.call(); 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) {}
}
}
} }
public void fetch() throws GitAPIException { public void fetch() throws GitAPIException {
@ -363,12 +364,12 @@ public class GitRepository {
private <C extends GitCommand<T>, T> T execute(C command) throws GitAPIException { private <C extends GitCommand<T>, T> T execute(C command) throws GitAPIException {
if (command instanceof TransportCommand) { if (command instanceof TransportCommand) {
configureTransportCommand((TransportCommand) command, credentialsProvider, sshSessionFactory); authHandler.configureCommand((TransportCommand) command);
} }
return command.call(); return command.call();
} }
private static Function<PageLink, Comparator<RevCommit>> revCommitComparatorFunction = pageLink -> { private static final Function<PageLink, Comparator<RevCommit>> revCommitComparatorFunction = pageLink -> {
SortOrder sortOrder = pageLink.getSortOrder(); SortOrder sortOrder = pageLink.getSortOrder();
if (sortOrder != null if (sortOrder != null
&& sortOrder.getProperty().equals("timestamp") && sortOrder.getProperty().equals("timestamp")
@ -405,12 +406,28 @@ public class GitRepository {
return new PageData<>(data, totalPages, totalElements, hasNext); return new PageData<>(data, totalPages, totalElements, hasNext);
} }
private static void configureTransportCommand(TransportCommand transportCommand, CredentialsProvider credentialsProvider, SshdSessionFactory 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);
}
protected void configureCommand(TransportCommand command) {
if (credentialsProvider != null) { if (credentialsProvider != null) {
transportCommand.setCredentialsProvider(credentialsProvider); command.setCredentialsProvider(credentialsProvider);
} }
if (sshSessionFactory != null) { if (sshSessionFactory != null) {
transportCommand.setTransportConfigCallback(transport -> { command.setTransportConfigCallback(transport -> {
if (transport instanceof SshTransport) { if (transport instanceof SshTransport) {
SshTransport sshTransport = (SshTransport) transport; SshTransport sshTransport = (SshTransport) transport;
sshTransport.setSshSessionFactory(sshSessionFactory); sshTransport.setSshSessionFactory(sshSessionFactory);
@ -459,6 +476,7 @@ public class GitRepository {
} }
return keyPairs; return keyPairs;
} }
}
private static class NoMergesAndCommitMessageFilter extends RevFilter { private static class NoMergesAndCommitMessageFilter extends RevFilter {

View File

@ -15,8 +15,8 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable, of } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { import {
AdminSettings, AdminSettings,
@ -24,12 +24,12 @@ import {
MailServerSettings, MailServerSettings,
SecuritySettings, SecuritySettings,
TestSmsRequest, TestSmsRequest,
UpdateMessage, AutoCommitSettings UpdateMessage,
AutoCommitSettings,
RepositorySettingsInfo
} from '@shared/models/settings.models'; } from '@shared/models/settings.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { AuthUser } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -97,6 +97,10 @@ export class AdminService {
return this.http.post<void>('/api/admin/repositorySettings/checkAccess', repositorySettings, defaultHttpOptionsFromConfig(config)); 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> { public getAutoCommitSettings(config?: RequestConfig): Observable<AutoCommitSettings> {
return this.http.get<AutoCommitSettings>(`/api/admin/autoCommitSettings`, defaultHttpOptionsFromConfig(config)); return this.http.get<AutoCommitSettings>(`/api/admin/autoCommitSettings`, defaultHttpOptionsFromConfig(config));
} }

View File

@ -29,7 +29,7 @@
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;"> <mat-card-content style="padding-top: 16px;">
<form [formGroup]="autoCommitSettingsForm" #formDirective="ngForm" (ngSubmit)="save()"> <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> <legend class="group-title" translate>admin.auto-commit-entities</legend>
<div fxLayout="column"> <div fxLayout="column">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType; <div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
@ -111,13 +111,14 @@
</div> </div>
</div> </div>
</fieldset> </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"> <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" <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}} {{'action.delete' | translate}}
</button> </button>
<span fxFlex></span> <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}} type="submit">{{'action.save' | translate}}
</button> </button>
</div> </div>

View File

@ -23,8 +23,8 @@ import { AdminService } from '@core/http/admin.service';
import { AutoCommitSettings, AutoVersionCreateConfig } from '@shared/models/settings.models'; import { AutoCommitSettings, AutoVersionCreateConfig } from '@shared/models/settings.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { catchError, mergeMap } from 'rxjs/operators'; import { catchError, map, mergeMap } from 'rxjs/operators';
import { of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { EntityTypeVersionCreateConfig, exportableEntityTypes } from '@shared/models/vc.models'; import { EntityTypeVersionCreateConfig, exportableEntityTypes } from '@shared/models/vc.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@ -41,6 +41,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
entityTypes = EntityType; entityTypes = EntityType;
isReadOnly: Observable<boolean>;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private adminService: AdminService, private adminService: AdminService,
private dialogService: DialogService, private dialogService: DialogService,
@ -71,6 +73,7 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
this.autoCommitSettingsForm.setControl('entityTypes', this.autoCommitSettingsForm.setControl('entityTypes',
this.prepareEntityTypesFormArray(settings), {emitEvent: false}); this.prepareEntityTypesFormArray(settings), {emitEvent: false});
}); });
this.isReadOnly = this.adminService.getRepositorySettingsInfo().pipe(map(settings => settings.readOnly));
} }
entityTypesFormGroupArray(): FormGroup[] { entityTypesFormGroupArray(): FormGroup[] {

View File

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

View File

@ -34,14 +34,14 @@
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center end"> <div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center end">
<button *ngIf="singleEntityMode" mat-stroked-button color="primary" <button *ngIf="singleEntityMode" mat-stroked-button color="primary"
#createVersionButton #createVersionButton
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async) || (isReadOnly | async)"
(click)="toggleCreateVersion($event, createVersionButton)"> (click)="toggleCreateVersion($event, createVersionButton)">
<mat-icon>update</mat-icon> <mat-icon>update</mat-icon>
{{'version-control.create-version' | translate }} {{'version-control.create-version' | translate }}
</button> </button>
<button *ngIf="!singleEntityMode" mat-stroked-button color="primary" <button *ngIf="!singleEntityMode" mat-stroked-button color="primary"
#complexCreateVersionButton #complexCreateVersionButton
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async) || (isReadOnly | async)"
(click)="toggleComplexCreateVersion($event, complexCreateVersionButton)"> (click)="toggleComplexCreateVersion($event, complexCreateVersionButton)">
<mat-icon>update</mat-icon> <mat-icon>update</mat-icon>
{{'version-control.create-entities-version' | translate }} {{'version-control.create-entities-version' | translate }}

View File

@ -54,6 +54,7 @@ import { EntityVersionDiffComponent } from '@home/components/vc/entity-version-d
import { ComplexVersionCreateComponent } from '@home/components/vc/complex-version-create.component'; import { ComplexVersionCreateComponent } from '@home/components/vc/complex-version-create.component';
import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version-load.component'; import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version-load.component';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { AdminService } from "@core/http/admin.service";
@Component({ @Component({
selector: 'tb-entity-versions-table', selector: 'tb-entity-versions-table',
@ -87,6 +88,8 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
viewsInited = false; viewsInited = false;
isReadOnly: Observable<boolean>;
private componentResize$: ResizeObserver; private componentResize$: ResizeObserver;
@Input() @Input()
@ -129,6 +132,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService, private entitiesVersionControlService: EntitiesVersionControlService,
private adminService: AdminService,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
@ -150,6 +154,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
} }
}); });
this.componentResize$.observe(this.elementRef.nativeElement); this.componentResize$.observe(this.elementRef.nativeElement);
this.isReadOnly = this.adminService.getRepositorySettingsInfo().pipe(map(settings => settings.readOnly));
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -41,6 +41,9 @@
<mat-label translate>admin.default-branch</mat-label> <mat-label translate>admin.default-branch</mat-label>
<input matInput formControlName="defaultBranch"> <input matInput formControlName="defaultBranch">
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="readOnly">
{{ 'admin.repository-read-only' | translate }}
</mat-checkbox>
<fieldset [disabled]="isLoading$ | async" class="fields-group"> <fieldset [disabled]="isLoading$ | async" class="fields-group">
<legend class="group-title" translate>admin.authentication-settings</legend> <legend class="group-title" translate>admin.authentication-settings</legend>
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
@ -58,7 +61,7 @@
autocomplete="new-username"/> autocomplete="new-username"/>
</mat-form-field> </mat-form-field>
<mat-checkbox *ngIf="showChangePassword" (change)="changePasswordChanged()" <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 }} {{ 'admin.change-password-access-token' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-form-field class="mat-block" *ngIf="changePassword || !showChangePassword"> <mat-form-field class="mat-block" *ngIf="changePassword || !showChangePassword">
@ -78,7 +81,7 @@
(fileNameChanged)="repositorySettingsForm.get('privateKeyFileName').patchValue($event)"> (fileNameChanged)="repositorySettingsForm.get('privateKeyFileName').patchValue($event)">
</tb-file-input> </tb-file-input>
<mat-checkbox *ngIf="showChangePrivateKeyPassword" (change)="changePrivateKeyPasswordChanged()" <mat-checkbox *ngIf="showChangePrivateKeyPassword" (change)="changePrivateKeyPasswordChanged()"
[(ngModel)]="changePrivateKeyPassword" [ngModelOptions]="{ standalone: true }" style="padding-bottom: 16px;"> [(ngModel)]="changePrivateKeyPassword" [ngModelOptions]="{ standalone: true }">
{{ 'admin.change-passphrase' | translate }} {{ 'admin.change-passphrase' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-form-field class="mat-block" *ngIf="changePrivateKeyPassword || !showChangePrivateKeyPassword"> <mat-form-field class="mat-block" *ngIf="changePrivateKeyPassword || !showChangePrivateKeyPassword">

View File

@ -17,6 +17,9 @@
mat-card.repository-settings { mat-card.repository-settings {
margin: 8px; margin: 8px;
} }
mat-checkbox {
padding-bottom: 16px;
}
.fields-group { .fields-group {
padding: 0 16px 8px; padding: 0 16px 8px;
margin-bottom: 10px; margin-bottom: 10px;

View File

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

View File

@ -419,6 +419,11 @@ export interface RepositorySettings {
privateKeyPassword: string; privateKeyPassword: string;
} }
export interface RepositorySettingsInfo {
configured: boolean;
readOnly: boolean;
}
export interface AutoVersionCreateConfig extends VersionCreateConfig { export interface AutoVersionCreateConfig extends VersionCreateConfig {
branch: string; branch: string;
} }

View File

@ -328,6 +328,7 @@
"repository-url": "Repository URL", "repository-url": "Repository URL",
"repository-url-required": "Repository URL is required.", "repository-url-required": "Repository URL is required.",
"default-branch": "Default branch name", "default-branch": "Default branch name",
"repository-read-only": "Read-only",
"authentication-settings": "Authentication settings", "authentication-settings": "Authentication settings",
"auth-method": "Authentication method", "auth-method": "Authentication method",
"auth-method-username-password": "Password / access token", "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>.", "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.", "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>.", "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": {
"widget-library": "Widgets Library", "widget-library": "Widgets Library",