This commit is contained in:
Maksym Trofimenko 2024-05-04 18:49:12 -04:00 committed by GitHub
commit beb7a0ea3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 570 additions and 31 deletions

View File

@ -468,7 +468,7 @@ misspell:
@echo checking misspell...
@find . -type d \( -path ./tests \) -prune -o -name '*.go' -print | xargs misspell -error
# golangci-lint binary installation or refer to https://golangci-lint.run/usage/install/#local-installation
# golangci-lint binary installation or refer to https://golangci-lint.run/usage/install/#local-installation
# curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
GOLANGCI_LINT := $(shell go env GOPATH)/bin/golangci-lint
lint:

View File

@ -6908,6 +6908,12 @@ definitions:
format: date-time
example: '2006-01-02T15:04:05Z'
description: The time when this operation is triggered.
client_ip:
type: string
description: Client IP address when this operation is triggered.
user_agent:
type: string
description: User agent during the operation.
Metadata:
type: object
properties:
@ -7946,6 +7952,16 @@ definitions:
x-nullable: true
x-omitempty: true
$ref: '#/definitions/AuthproxySetting'
audit_log_track_ip_address:
type: boolean
x-nullable: true
x-omitempty: true
description: The flag to indicate whether IP address tracking is on in audit logs.
audit_log_track_user_agent:
type: boolean
x-nullable: true
x-omitempty: true
description: The flag to indicate whether user agent tracking is on in audit logs.
oidc_provider_name:
type: string
x-nullable: true
@ -9017,6 +9033,12 @@ definitions:
skip_audit_log_database:
$ref: '#/definitions/BoolConfigItem'
description: Whether skip the audit log in database
audit_log_track_ip_address:
$ref: '#/definitions/BoolConfigItem'
description: Whether client ip address tracking is enabled in audit logs
audit_log_track_user_agent:
$ref: '#/definitions/BoolConfigItem'
description: Whether user agent tracking is enabled in audit logs
scanner_skip_update_pulltime:
$ref: '#/definitions/BoolConfigItem'
description: Whether or not to skip update the pull time for scanner
@ -9297,6 +9319,16 @@ definitions:
description: Skip audit log database
x-omitempty: true
x-isnullable: true
audit_log_track_ip_address:
type: boolean
description: Track IP addresses in audit logs
x-omitempty: true
x-isnullable: true
audit_log_track_user_agent:
type: boolean
description: Track user agent in audit logs
x-omitempty: true
x-isnullable: true
session_timeout:
type: integer
description: The session timeout for harbor, in minutes.

View File

@ -1,3 +1,6 @@
ALTER TABLE audit_log ADD user_agent VARCHAR(255);
ALTER TABLE audit_log ADD client_ip inet;
/*
table artifact:
id SERIAL PRIMARY KEY NOT NULL,
@ -28,4 +31,4 @@ then set column artifact_type as not null
*/
UPDATE artifact SET artifact_type = media_type WHERE artifact_type IS NULL;
ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT NULL;
ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT NULL;

View File

@ -214,6 +214,12 @@ const (
AuditLogForwardEndpoint = "audit_log_forward_endpoint"
// SkipAuditLogDatabase skip to log audit log in database
SkipAuditLogDatabase = "skip_audit_log_database"
// AuditLogTrackIPAddress track client ip address with audit_logs
AuditLogTrackIPAddress = "audit_log_track_ip_address"
// AuditLogTrackUserAgent track user agent with audit_logs
AuditLogTrackUserAgent = "audit_log_track_user_agent"
// MaxAuditRetentionHour allowed in audit log purge
MaxAuditRetentionHour = 240000
// ScannerSkipUpdatePullTime

View File

@ -18,6 +18,7 @@ import (
"context"
"github.com/goharbor/harbor/src/controller/event"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/audit"
@ -40,7 +41,6 @@ func (h *Handler) Name() string {
// Handle ...
func (h *Handler) Handle(ctx context.Context, value interface{}) error {
var auditLog *am.AuditLog
var addAuditLog bool
switch v := value.(type) {
case *event.PushArtifactEvent, *event.DeleteArtifactEvent,
@ -60,9 +60,17 @@ func (h *Handler) Handle(ctx context.Context, value interface{}) error {
log.Errorf("failed to handler event %v", err)
return err
}
auditLog = al
if auditLog != nil {
_, err := audit.Mgr.Create(ctx, auditLog)
if al != nil {
ip := lib.GetClientIPAddress(ctx)
if ip != "" {
al.ClientIP = &ip
}
ua := lib.GetUserAgent(ctx)
if ua != "" {
al.UserAgent = &ua
}
_, err := audit.Mgr.Create(ctx, al)
if err != nil {
log.Debugf("add audit log err: %v", err)
}

View File

@ -49,10 +49,16 @@ type Data struct {
HarborVersion string
BannerMessage string
AuthProxySettings *models.HTTPAuthProxy
AuditLogs AuditLogSettings
Protected *protectedData
OIDCProviderName string
}
type AuditLogSettings struct {
TrackIPAddress bool
TrackUserAgent bool
}
type protectedData struct {
CurrentTime time.Time
RegistryURL string
@ -105,6 +111,10 @@ func (c *controller) GetInfo(ctx context.Context, opt Options) (*Data, error) {
HarborVersion: fmt.Sprintf("%s-%s", version.ReleaseVersion, version.GitCommit),
BannerMessage: utils.SafeCastString(mgr.Get(ctx, common.BannerMessage).GetString()),
OIDCProviderName: OIDCProviderName(cfg),
AuditLogs: AuditLogSettings{
TrackIPAddress: utils.SafeCastBool(cfg[common.AuditLogTrackIPAddress]),
TrackUserAgent: utils.SafeCastBool(cfg[common.AuditLogTrackUserAgent]),
},
}
if res.AuthMode == common.HTTPAuth {
if s, err := config.HTTPAuthProxySetting(ctx); err == nil {

View File

@ -18,6 +18,8 @@ import (
"net/http"
"regexp"
"github.com/goharbor/harbor/src/server/middleware/clientinfo"
"github.com/beego/beego/v2/server/web"
"github.com/goharbor/harbor/src/pkg/distribution"
@ -94,6 +96,7 @@ func MiddleWares() []web.MiddleWare {
session.Middleware(),
csrf.Middleware(),
orm.Middleware(pingSkipper),
clientinfo.Middleware(pingSkipper),
notification.Middleware(pingSkipper), // notification must ahead of transaction ensure the DB transaction execution complete
transaction.Middleware(dbTxSkippers...),
artifactinfo.Middleware(),

View File

@ -78,7 +78,7 @@ func GetManager(name string) (Manager, error) {
func DefaultMgr() Manager {
manager, err := GetManager(DefaultCfgManager)
if err != nil {
log.Error("failed to get config manager")
log.Error("failed to get config manager", err)
}
return manager
}

View File

@ -189,6 +189,9 @@ var (
{Name: common.AuditLogForwardEndpoint, Scope: UserScope, Group: BasicGroup, EnvKey: "AUDIT_LOG_FORWARD_ENDPOINT", DefaultValue: "", ItemType: &StringType{}, Editable: false, Description: `The endpoint to forward the audit log.`},
{Name: common.SkipAuditLogDatabase, Scope: UserScope, Group: BasicGroup, EnvKey: "SKIP_LOG_AUDIT_DATABASE", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `The option to skip audit log in database`},
{Name: common.AuditLogTrackIPAddress, Scope: UserScope, Group: BasicGroup, EnvKey: "AUDIT_LOG_TRACK_IP_ADDRESS", DefaultValue: "false", ItemType: &BoolType{}, Editable: true, Description: `The flag to enable IP addresses tracking in audit logs.`},
{Name: common.AuditLogTrackUserAgent, Scope: UserScope, Group: BasicGroup, EnvKey: "AUDIT_LOG_TRACK_USER_AGENT", DefaultValue: "false", ItemType: &BoolType{}, Editable: true, Description: `The flag to enable user agent tracking in audit logs.`},
{Name: common.ScannerSkipUpdatePullTime, Scope: UserScope, Group: BasicGroup, EnvKey: "SCANNER_SKIP_UPDATE_PULL_TIME", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `The option to skip update pull time for scanner`},
{Name: common.SessionTimeout, Scope: UserScope, Group: BasicGroup, EnvKey: "SESSION_TIMEOUT", DefaultValue: "60", ItemType: &Int64Type{}, Editable: true, Description: `The session timeout in minutes`},

View File

@ -251,6 +251,16 @@ func SkipAuditLogDatabase(ctx context.Context) bool {
return DefaultMgr().Get(ctx, common.SkipAuditLogDatabase).GetBool()
}
// AuditLogTrackIPAddress enables ip address tracking
func AuditLogTrackIPAddress(ctx context.Context) bool {
return DefaultMgr().Get(ctx, common.AuditLogTrackIPAddress).GetBool()
}
// AuditLogTrackUserAgent enables user info tracking
func AuditLogTrackUserAgent(ctx context.Context) bool {
return DefaultMgr().Get(ctx, common.AuditLogTrackUserAgent).GetBool()
}
// ScannerSkipUpdatePullTime returns the scanner skip update pull time setting
func ScannerSkipUpdatePullTime(ctx context.Context) bool {
return DefaultMgr().Get(ctx, common.ScannerSkipUpdatePullTime).GetBool()

View File

@ -27,6 +27,8 @@ const (
contextKeyAuthMode contextKey = "authMode"
contextKeyCarrySession contextKey = "carrySession"
contextKeyRequestID contextKey = "X-Request-ID"
contextClientIPAddress contextKey = "clientIPAddress"
contextUserAgent contextKey = "userAgent"
)
// ArtifactInfo wraps the artifact info extracted from the request to "/v2/"
@ -128,3 +130,33 @@ func GetXRequestID(ctx context.Context) string {
}
return id
}
// WithClientIPAddress returns a context with ipAddress set
func WithClientIPAddress(ctx context.Context, ipAddress string) context.Context {
return setToContext(ctx, contextClientIPAddress, ipAddress)
}
// WithUserAgent returns a context with user agent set
func WithUserAgent(ctx context.Context, userAgent string) context.Context {
return setToContext(ctx, contextUserAgent, userAgent)
}
// GetClientIPAddress gets the ip address from the context
func GetClientIPAddress(ctx context.Context) string {
var result string
value := getFromContext(ctx, contextClientIPAddress)
if value != nil {
result, _ = value.(string)
}
return result
}
// GetUserAgent gets the user agent from the context
func GetUserAgent(ctx context.Context) string {
var result string
value := getFromContext(ctx, contextUserAgent)
if value != nil {
result, _ = value.(string)
}
return result
}

View File

@ -77,9 +77,15 @@ func (m *manager) Get(ctx context.Context, id int64) (*model.AuditLog, error) {
// Create ...
func (m *manager) Create(ctx context.Context, audit *model.AuditLog) (int64, error) {
if len(config.AuditLogForwardEndpoint(ctx)) > 0 {
LogMgr.DefaultLogger(ctx).WithField("operator", audit.Username).
WithField("time", audit.OpTime).WithField("resourceType", audit.ResourceType).
Infof("action:%s, resource:%s", audit.Operation, audit.Resource)
logger := LogMgr.DefaultLogger(ctx).WithField("operator", audit.Username).
WithField("time", audit.OpTime).WithField("resourceType", audit.ResourceType)
if config.AuditLogTrackIPAddress(ctx) && audit.ClientIP != nil {
logger.WithField("clientIP", *audit.ClientIP)
}
if config.AuditLogTrackUserAgent(ctx) && audit.UserAgent != nil {
logger.WithField("userAgent", *audit.UserAgent)
}
logger.Infof("action:%s, resource:%s", audit.Operation, audit.Resource)
}
if config.SkipAuditLogDatabase(ctx) {
return 0, nil

View File

@ -33,6 +33,8 @@ type AuditLog struct {
Resource string `orm:"column(resource)" json:"resource"`
Username string `orm:"column(username)" json:"username"`
OpTime time.Time `orm:"column(op_time)" json:"op_time" sort:"default:desc"`
UserAgent *string `orm:"column(user_agent)" json:"user_agent"`
ClientIP *string `orm:"column(client_ip)" json:"client_ip"`
}
// TableName for audit log

View File

@ -112,6 +112,8 @@ export class Configuration {
oidc_group_filter: StringValueItem;
audit_log_forward_endpoint: StringValueItem;
skip_audit_log_database: BoolValueItem;
audit_log_track_ip_address: BoolValueItem;
audit_log_track_user_agent: BoolValueItem;
session_timeout: NumberValueItem;
scanner_skip_update_pulltime: BoolValueItem;
banner_message: StringValueItem;
@ -189,6 +191,8 @@ export class Configuration {
this.storage_per_project = new NumberValueItem(-1, true);
this.audit_log_forward_endpoint = new StringValueItem('', true);
this.skip_audit_log_database = new BoolValueItem(false, true);
this.audit_log_track_ip_address = new BoolValueItem(false, true);
this.audit_log_track_user_agent = new BoolValueItem(false, true);
this.session_timeout = new NumberValueItem(60, true);
this.scanner_skip_update_pulltime = new BoolValueItem(false, true);
this.banner_message = new StringValueItem(

View File

@ -346,6 +346,64 @@
" />
</clr-checkbox-wrapper>
</clr-checkbox-container>
<clr-checkbox-container class="center">
<label for="auditLogTrackIpAddress"
>{{ 'AUDIT_LOG.TRACK_IP' | translate }}
<clr-tooltip>
<clr-icon
clrTooltipTrigger
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
clrPosition="top-right"
clrSize="lg"
*clrIfOpen>
<span>{{
'AUDIT_LOG.TRACK_IP_TOOLTIP' | translate
}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<clr-checkbox-wrapper>
<input
type="checkbox"
clrCheckbox
name="auditLogTrackIpAddress"
id="auditLogTrackIpAddress"
[(ngModel)]="
currentConfig.audit_log_track_ip_address.value
" />
</clr-checkbox-wrapper>
</clr-checkbox-container>
<clr-checkbox-container class="center">
<label for="auditLogTrackUserAgent"
>{{ 'AUDIT_LOG.TRACK_UA' | translate }}
<clr-tooltip>
<clr-icon
clrTooltipTrigger
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
clrPosition="top-right"
clrSize="lg"
*clrIfOpen>
<span>{{
'AUDIT_LOG.TRACK_UA_TOOLTIP' | translate
}}</span>
</clr-tooltip-content>
</clr-tooltip>
</label>
<clr-checkbox-wrapper>
<input
type="checkbox"
clrCheckbox
name="auditLogTrackUserAgent"
id="auditLogTrackUserAgent"
[(ngModel)]="
currentConfig.audit_log_track_user_agent.value
" />
</clr-checkbox-wrapper>
</clr-checkbox-container>
<div class="clr-form-control">
<label class="clr-control-label">{{
'BANNER_MESSAGE.BANNER_MESSAGE' | translate

View File

@ -188,6 +188,8 @@ export class SystemSettingsComponent implements OnInit, OnDestroy {
prop === 'robot_name_prefix' ||
prop === 'audit_log_forward_endpoint' ||
prop === 'skip_audit_log_database' ||
prop === 'audit_log_track_ip_address' ||
prop === 'audit_log_track_user_agent' ||
prop === 'session_timeout' ||
prop === 'scanner_skip_update_pulltime' ||
prop === 'banner_message'

View File

@ -56,6 +56,12 @@
<clr-dg-column>{{
'AUDIT_LOG.OPERATION' | translate
}}</clr-dg-column>
<clr-dg-column *ngIf="isIPTracked()">{{
'AUDIT_LOG.IP_ADDRESS' | translate
}}</clr-dg-column>
<clr-dg-column *ngIf="isUATracked()">{{
'AUDIT_LOG.USER_AGENT' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.TIMESTAMP' | translate
}}</clr-dg-column>
@ -67,6 +73,12 @@
<clr-dg-cell>{{ l.resource }}</clr-dg-cell>
<clr-dg-cell>{{ l.resource_type }}</clr-dg-cell>
<clr-dg-cell>{{ l.operation }}</clr-dg-cell>
<clr-dg-cell *ngIf="isIPTracked()">{{
l.client_ip
}}</clr-dg-cell>
<clr-dg-cell *ngIf="isUATracked()">{{
l.user_agent
}}</clr-dg-cell>
<clr-dg-cell>{{
l.op_time | harborDatetime : 'short'
}}</clr-dg-cell>

View File

@ -10,6 +10,7 @@ import { AuditlogService } from '../../../../../ng-swagger-gen/services/auditlog
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { delay } from 'rxjs/operators';
import { SharedTestingModule } from '../../../shared/shared.module';
import { AppConfigService } from '../../../services/app-config.service';
describe('RecentLogComponent (inline template)', () => {
let component: RecentLogComponent;
@ -20,6 +21,9 @@ describe('RecentLogComponent (inline template)', () => {
return undefined;
},
};
let fakeAppConfigService = {
handleError: () => {},
};
const mockedAuditLogs: AuditLog[] = [];
for (let i = 0; i < 18; i++) {
let item: AuditLog = {
@ -83,6 +87,7 @@ describe('RecentLogComponent (inline template)', () => {
providers: [
{ provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: AuditlogService, useValue: fakedAuditlogService },
{ provide: AppConfigService, useValue: fakeAppConfigService },
],
}).compileComponents();
});

View File

@ -17,6 +17,7 @@ import { finalize } from 'rxjs/operators';
import { AuditlogService } from '../../../../../ng-swagger-gen/services/auditlog.service';
import { AuditLog } from '../../../../../ng-swagger-gen/models/audit-log';
import { ClrDatagridStateInterface } from '@clr/angular';
import { AppConfigService } from '../../../services/app-config.service';
import {
getPageSizeFromLocalStorage,
PageSizeMapKeys,
@ -43,7 +44,8 @@ export class RecentLogComponent {
constructor(
private logService: AuditlogService,
private errorHandler: ErrorHandler
private errorHandler: ErrorHandler,
private appConfigService: AppConfigService
) {}
public get inProgress(): boolean {
@ -74,7 +76,18 @@ export class RecentLogComponent {
this.defaultFilter = $event['target'].value;
this.doFilter(this.currentTerm);
}
isIPTracked(): boolean {
if (this.appConfigService?.configurations?.audit_log_track_ip_address) {
return true;
}
return false;
}
isUATracked(): boolean {
if (this.appConfigService?.configurations?.audit_log_track_user_agent) {
return true;
}
return false;
}
load(state?: ClrDatagridStateInterface) {
if (state && state.page) {
this.pageSize = state.page.size;

View File

@ -210,6 +210,14 @@ describe('ProjectComponent', () => {
value: false,
editable: true,
},
audit_log_track_ip_address: {
value: false,
editable: true,
},
audit_log_track_user_agent: {
value: false,
editable: true,
},
});
},
};

View File

@ -69,6 +69,12 @@
<clr-dg-column>{{
'AUDIT_LOG.OPERATION' | translate
}}</clr-dg-column>
<clr-dg-column *ngIf="isIPTracked()">{{
'AUDIT_LOG.IP_ADDRESS' | translate
}}</clr-dg-column>
<clr-dg-column *ngIf="isUATracked()">{{
'AUDIT_LOG.USER_AGENT' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'AUDIT_LOG.TIMESTAMP' | translate
}}</clr-dg-column>
@ -77,6 +83,12 @@
<clr-dg-cell>{{ l.resource }}</clr-dg-cell>
<clr-dg-cell>{{ l.resource_type }}</clr-dg-cell>
<clr-dg-cell>{{ l.operation }}</clr-dg-cell>
<clr-dg-cell *ngIf="isIPTracked()">{{
l.client_ip
}}</clr-dg-cell>
<clr-dg-cell *ngIf="isUATracked()">{{
l.user_agent
}}</clr-dg-cell>
<clr-dg-cell>{{
l.op_time | harborDatetime : 'short'
}}</clr-dg-cell>

View File

@ -13,6 +13,7 @@ import { SharedTestingModule } from '../../../shared/shared.module';
import { registerLocaleData } from '@angular/common';
import locale_en from '@angular/common/locales/en';
import { DatePickerComponent } from '../../../shared/components/datetime-picker/datetime-picker.component';
import { AppConfigService } from '../../../services/app-config.service';
describe('AuditLogComponent', () => {
let component: AuditLogComponent;
@ -20,6 +21,9 @@ describe('AuditLogComponent', () => {
const mockMessageHandlerService = {
handleError: () => {},
};
let fakeAppConfigService = {
handleError: () => {},
};
const mockActivatedRoute = {
parent: {
parent: {
@ -105,6 +109,7 @@ describe('AuditLogComponent', () => {
provide: MessageHandlerService,
useValue: mockMessageHandlerService,
},
{ provide: AppConfigService, useValue: fakeAppConfigService },
],
}).compileComponents();
});

View File

@ -25,6 +25,7 @@ import {
setPageSizeToLocalStorage,
} from '../../../shared/units/utils';
import { ClrDatagridStateInterface } from '@clr/angular';
import { AppConfigService } from '../../../services/app-config.service';
const optionalSearch: {} = { 0: 'AUDIT_LOG.ADVANCED', 1: 'AUDIT_LOG.SIMPLE' };
@ -104,7 +105,8 @@ export class AuditLogComponent implements OnInit {
private route: ActivatedRoute,
private router: Router,
private auditLogService: ProjectService,
private messageHandlerService: MessageHandlerService
private messageHandlerService: MessageHandlerService,
private appConfigService: AppConfigService
) {
// Get current user from registered resolver.
this.route.data.subscribe(
@ -120,6 +122,19 @@ export class AuditLogComponent implements OnInit {
}
}
isIPTracked(): boolean {
if (this.appConfigService?.configurations?.audit_log_track_ip_address) {
return true;
}
return false;
}
isUATracked(): boolean {
if (this.appConfigService?.configurations?.audit_log_track_user_agent) {
return true;
}
return false;
}
retrieve(state?: ClrDatagridStateInterface) {
if (state && state.page) {
this.pageSize = state.page.size;

View File

@ -19,6 +19,8 @@ export class AppConfig {
admiral_endpoint: string;
auth_mode: string;
primary_auth_mode: boolean;
audit_log_track_ip_address: boolean;
audit_log_track_user_agent: boolean;
registry_url: string;
project_creation_restriction: string;
self_registration: boolean;

View File

@ -534,7 +534,11 @@
"OF": "von",
"NOT_FOUND": "Es konnten keine Logdaten gefunden werden!",
"RESOURCE": "Ressource",
"RESOURCE_TYPE": "Ressourcen-Typ"
"RESOURCE_TYPE": "Ressourcen Typ",
"TRACK_IP": "IP-Adresse im Audit-Log",
"TRACK_IP_TOOLTIP": "Die IP-Adresse des Clients wird bei jeder Anfrage erfasst und im Audit-Protokoll gespeichert.",
"TRACK_UA": "Benutzer-Agent im Audit-Log",
"TRACK_UA_TOOLTIP": "Der Benutzer-Agent des Clients wird bei jeder Anfrage erfasst und im Audit-Protokoll gespeichert."
},
"REPLICATION": {
"PUSH_BASED_ONLY": "Only for the push-based replication",

View File

@ -535,7 +535,13 @@
"OF": "of",
"NOT_FOUND": "We couldn't find any logs!",
"RESOURCE": "Resource",
"RESOURCE_TYPE": "Resource Type"
"RESOURCE_TYPE": "Resource Type",
"IP_ADDRESS": "IP Address",
"USER_AGENT": "User-Agent",
"TRACK_IP": "IP-Address in Audit Log",
"TRACK_IP_TOOLTIP": "Capture the client's IP addresses on each request and store them in the audit log.",
"TRACK_UA": "User-Agent in Audit Log",
"TRACK_UA_TOOLTIP": "Capture the client's User-Agent on each request and store it in the audit log."
},
"REPLICATION": {
"PUSH_BASED_ONLY": "Only for the push-based replication",

View File

@ -534,7 +534,11 @@
"OF": "of",
"NOT_FOUND": "No pudimos encontrar ningún registro!",
"RESOURCE": "Resource",
"RESOURCE_TYPE": "Resource Type"
"RESOURCE_TYPE": "Resource Type",
"TRACK_IP": "IP-Address in Audit Log",
"TRACK_IP_TOOLTIP": "Capture the client's IP addresses on each request and store them in the audit log.",
"TRACK_UA": "User-Agent in Audit Log",
"TRACK_UA_TOOLTIP": "Capture the client's User-Agent on each request and store it in the audit log."
},
"REPLICATION": {
"PUSH_BASED_ONLY": "Only for the push-based replication",

View File

@ -534,7 +534,11 @@
"OF": "sur",
"NOT_FOUND": "Nous n'avons trouvé aucun log !",
"RESOURCE": "Ressource",
"RESOURCE_TYPE": "Type de ressource"
"RESOURCE_TYPE": "Type de ressource",
"TRACK_IP": "IP-Address in Audit Log",
"TRACK_IP_TOOLTIP": "Capture the client's IP addresses on each request and store them in the audit log.",
"TRACK_UA": "User-Agent in Audit Log",
"TRACK_UA_TOOLTIP": "Capture the client's User-Agent on each request and store it in the audit log."
},
"REPLICATION": {
"PUSH_BASED_ONLY": "Uniquement pour la réplication de type push",

View File

@ -532,7 +532,11 @@
"OF": "de",
"NOT_FOUND": "Não encontramos nenhum registro!",
"RESOURCE": "Recurso",
"RESOURCE_TYPE": "Tipo de Recurso"
"RESOURCE_TYPE": "Tipo de Recurso",
"TRACK_IP": "IP-Address in Audit Log",
"TRACK_IP_TOOLTIP": "Capture the client's IP addresses on each request and store them in the audit log.",
"TRACK_UA": "User-Agent in Audit Log",
"TRACK_UA_TOOLTIP": "Capture the client's User-Agent on each request and store it in the audit log."
},
"REPLICATION": {
"PUSH_BASED_ONLY": "Only for the push-based replication",

View File

@ -534,7 +534,11 @@
"OF": "of",
"NOT_FOUND": "We couldn't find any logs!",
"RESOURCE": "Resource",
"RESOURCE_TYPE": "Resource Type"
"RESOURCE_TYPE": "Resource Type",
"TRACK_IP": "IP-Address in Audit Log",
"TRACK_IP_TOOLTIP": "Capture the client's IP addresses on each request and store them in the audit log.",
"TRACK_UA": "User-Agent in Audit Log",
"TRACK_UA_TOOLTIP": "Capture the client's User-Agent on each request and store it in the audit log."
},
"REPLICATION": {
"PUSH_BASED_ONLY": "Only for the push-based replication",

View File

@ -532,7 +532,11 @@
"OF": "共计",
"NOT_FOUND": "未发现任何日志!",
"RESOURCE": "资源",
"RESOURCE_TYPE": "资源类型"
"RESOURCE_TYPE": "资源类型",
"TRACK_IP": "IP-Address in Audit Log",
"TRACK_IP_TOOLTIP": "Capture the client's IP addresses on each request and store them in the audit log.",
"TRACK_UA": "User-Agent in Audit Log",
"TRACK_UA_TOOLTIP": "Capture the client's User-Agent on each request and store it in the audit log."
},
"REPLICATION": {
"PUSH_BASED_ONLY": "仅针对于 Push-Based 模式下的复制操作",

View File

@ -533,7 +533,11 @@
"OF": "共計",
"NOT_FOUND": "未發現任何日誌!",
"RESOURCE": "資源",
"RESOURCE_TYPE": "資源類型"
"RESOURCE_TYPE": "資源類型",
"TRACK_IP": "IP-Address in Audit Log",
"TRACK_IP_TOOLTIP": "Capture the client's IP addresses on each request and store them in the audit log.",
"TRACK_UA": "User-Agent in Audit Log",
"TRACK_UA_TOOLTIP": "Capture the client's User-Agent on each request and store it in the audit log."
},
"REPLICATION": {
"PUSH_BASED_ONLY": "Only for the push-based replication",

View File

@ -0,0 +1,86 @@
// Copyright Project Harbor 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 clientinfo
import (
"net/http"
"regexp"
"strings"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/server/middleware"
)
var (
// De-facto standard header keys.
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
// RFC7239 defines a new "Forwarded: " header designed to replace the
// existing use of X-Forwarded-* headers.
// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
forwarded = http.CanonicalHeaderKey("Forwarded")
// Allows for a sub-match of the first value after 'for=' to the next
// comma, semi-colon or space. The match is case-insensitive.
forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)`)
)
func getIP(r *http.Request) string {
var addr string
if fwd := r.Header.Get(xForwardedFor); fwd != "" {
// Only grab the first (client) address. Note that '192.168.0.1,
// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
// the first may represent forwarding proxies earlier in the chain.
s := strings.Index(fwd, ", ")
if s == -1 {
s = len(fwd)
}
addr = fwd[:s]
} else if fwd := r.Header.Get(xRealIP); fwd != "" {
// X-Real-IP should only contain one IP address (the client making the
// request).
addr = fwd
} else if fwd := r.Header.Get(forwarded); fwd != "" {
// match should contain at least two elements if the protocol was
// specified in the Forwarded header. The first element will always be
// the 'for=' capture, which we ignore. In the case of multiple IP
// addresses (for=8.8.8.8, 8.8.4.4,172.16.1.20 is valid) we only
// extract the first, which should be the client IP.
if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
// IPv6 addresses in Forwarded headers are quoted-strings. We strip
// these quotes.
addr = strings.Trim(match[1], `"`)
}
}
return addr
}
// Middleware sends the notification after transaction success
func Middleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler {
return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
ctx := r.Context()
if config.AuditLogTrackIPAddress(ctx) {
ctx = lib.WithClientIPAddress(ctx, getIP(r))
}
if config.AuditLogTrackUserAgent(ctx) {
ctx = lib.WithUserAgent(ctx, r.UserAgent())
}
next.ServeHTTP(w, r.WithContext(ctx))
}, skippers...)
}

View File

@ -0,0 +1,152 @@
package clientinfo
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/config"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
"github.com/stretchr/testify/assert"
)
func TestMiddleware(t *testing.T) {
cases := []struct {
name string
init func()
teardown func()
buildReq func() *http.Request
checkReq func(t *testing.T, r *http.Request)
}{
{
name: "test ip track on using X-Real-IP",
buildReq: func() *http.Request {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("X-Real-IP", "192.168.0.1")
return r
},
checkReq: func(t *testing.T, r *http.Request) {
assert.Equal(t, "192.168.0.1", lib.GetClientIPAddress(r.Context()))
},
init: func() {
config.InitWithSettings(map[string]interface{}{
common.AuditLogTrackIPAddress: "true",
})
},
teardown: func() {
config.InitWithSettings(test.GetUnitTestConfig())
},
},
{
name: "test ip track on using X-Forwarded-For",
buildReq: func() *http.Request {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("X-Forwarded-For", "192.168.0.1")
return r
},
checkReq: func(t *testing.T, r *http.Request) {
assert.Equal(t, "192.168.0.1", lib.GetClientIPAddress(r.Context()))
},
init: func() {
config.InitWithSettings(map[string]interface{}{
common.AuditLogTrackIPAddress: "true",
})
},
teardown: func() {
config.InitWithSettings(test.GetUnitTestConfig())
},
},
{
name: "test ip track on, X-Forwarded-For superiors X-Real-IP",
buildReq: func() *http.Request {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("X-Real-IP", "192.168.0.10")
r.Header.Set("X-Forwarded-For", "192.168.0.1")
return r
},
checkReq: func(t *testing.T, r *http.Request) {
assert.Equal(t, "192.168.0.1", lib.GetClientIPAddress(r.Context()))
},
init: func() {
config.InitWithSettings(map[string]interface{}{
common.AuditLogTrackIPAddress: "true",
})
},
teardown: func() {
config.InitWithSettings(test.GetUnitTestConfig())
},
},
{
name: "test ip track off using X-Real-IP",
buildReq: func() *http.Request {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("X-Real-IP", "192.168.0.1")
return r
},
checkReq: func(t *testing.T, r *http.Request) {
assert.Equal(t, "", lib.GetClientIPAddress(r.Context()))
},
init: func() {
config.InitWithSettings(map[string]interface{}{
common.AuditLogTrackIPAddress: "false",
})
},
teardown: func() {
config.InitWithSettings(test.GetUnitTestConfig())
},
},
{
name: "test user agent track on",
buildReq: func() *http.Request {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("User-Agent", "harbor-test")
return r
},
checkReq: func(t *testing.T, r *http.Request) {
assert.Equal(t, "harbor-test", lib.GetUserAgent(r.Context()))
},
init: func() {
config.InitWithSettings(map[string]interface{}{
common.AuditLogTrackUserAgent: "true",
})
},
teardown: func() {
config.InitWithSettings(test.GetUnitTestConfig())
},
},
{
name: "test user agent track off",
buildReq: func() *http.Request {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("User-Agent", "harbor-test")
return r
},
checkReq: func(t *testing.T, r *http.Request) {
assert.Equal(t, "", lib.GetUserAgent(r.Context()))
},
init: func() {
config.InitWithSettings(map[string]interface{}{
common.AuditLogTrackUserAgent: "false",
})
},
teardown: func() {
config.InitWithSettings(test.GetUnitTestConfig())
},
},
}
for _, c := range cases {
var r *http.Request
c.init()
Middleware()(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
r = req
})).ServeHTTP(httptest.NewRecorder(), c.buildReq())
c.teardown()
t.Run(c.name, func(t *testing.T) {
c.checkReq(t, r)
})
}
}

View File

@ -96,14 +96,21 @@ func (a *auditlogAPI) ListAuditLogs(ctx context.Context, params auditlog.ListAud
var auditLogs []*models.AuditLog
for _, log := range logs {
auditLogs = append(auditLogs, &models.AuditLog{
al := &models.AuditLog{
ID: log.ID,
Resource: log.Resource,
ResourceType: log.ResourceType,
Username: log.Username,
Operation: log.Operation,
OpTime: strfmt.DateTime(log.OpTime),
})
}
if log.UserAgent != nil {
al.UserAgent = *log.UserAgent
}
if log.ClientIP != nil {
al.ClientIP = *log.ClientIP
}
auditLogs = append(auditLogs, al)
}
return auditlog.NewListAuditLogsOK().
WithXTotalCount(total).

View File

@ -315,14 +315,21 @@ func (a *projectAPI) GetLogs(ctx context.Context, params operation.GetLogsParams
var auditLogs []*models.AuditLog
for _, log := range logs {
auditLogs = append(auditLogs, &models.AuditLog{
al := &models.AuditLog{
ID: log.ID,
Resource: log.Resource,
ResourceType: log.ResourceType,
Username: log.Username,
Operation: log.Operation,
OpTime: strfmt.DateTime(log.OpTime),
})
}
if log.UserAgent != nil {
al.UserAgent = *log.UserAgent
}
if log.ClientIP != nil {
al.ClientIP = *log.ClientIP
}
auditLogs = append(auditLogs, al)
}
return operation.NewGetLogsOK().
WithXTotalCount(total).

View File

@ -82,12 +82,14 @@ func (s *sysInfoAPI) convertInfo(d *si.Data) *models.GeneralInfo {
return nil
}
res := &models.GeneralInfo{
AuthMode: &d.AuthMode,
PrimaryAuthMode: &d.PrimaryAuthMode,
SelfRegistration: &d.SelfRegistration,
HarborVersion: &d.HarborVersion,
BannerMessage: &d.BannerMessage,
OIDCProviderName: &d.OIDCProviderName,
AuthMode: &d.AuthMode,
PrimaryAuthMode: &d.PrimaryAuthMode,
SelfRegistration: &d.SelfRegistration,
HarborVersion: &d.HarborVersion,
BannerMessage: &d.BannerMessage,
OIDCProviderName: &d.OIDCProviderName,
AuditLogTrackIPAddress: &d.AuditLogs.TrackIPAddress,
AuditLogTrackUserAgent: &d.AuditLogs.TrackUserAgent,
}
if d.AuthProxySettings != nil {
res.AuthproxySettings = &models.AuthproxySetting{