mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-13 01:30:32 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0e8f8d813 | |||
| 1c342c2a19 | |||
| 12d4a28d34 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -144,7 +144,7 @@ jobs:
|
||||
|
||||
- name: Smoke test — verify alpine image starts and responds
|
||||
run: |
|
||||
docker rm -f nora-smoke 2>/dev/null || true
|
||||
docker rm -f nora-smoke 2>/dev/null || echo "WARNING: attestation failed, continuing without provenance"
|
||||
docker run --rm -d --name nora-smoke -p 5555:4000 -e NORA_HOST=0.0.0.0 \
|
||||
${{ env.NORA }}/${{ env.IMAGE_NAME }}:latest
|
||||
for i in $(seq 1 10); do
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
cat nora-linux-amd64.sha256
|
||||
|
||||
- name: Generate SLSA provenance
|
||||
uses: slsa-framework/slsa-github-generator/.github/actions/generate-builder@v2.1.0
|
||||
uses: slsa-framework/slsa-github-generator/.github/actions/generate-builder@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0
|
||||
id: provenance-generate
|
||||
continue-on-error: true
|
||||
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
if: always()
|
||||
run: |
|
||||
# Generate provenance using gh attestation (built-in GitHub feature)
|
||||
gh attestation create ./nora-linux-amd64 --repo ${{ github.repository }} --signer-workflow ${{ github.server_url }}/${{ github.repository }}/.github/workflows/release.yml 2>/dev/null || true
|
||||
gh attestation create ./nora-linux-amd64 --repo ${{ github.repository }} --signer-workflow ${{ github.server_url }}/${{ github.repository }}/.github/workflows/release.yml 2>/dev/null || echo "WARNING: attestation failed, continuing without provenance"
|
||||
# Also create a simple provenance file for scorecard
|
||||
cat > nora-v${{ github.ref_name }}.provenance.json << 'PROVEOF'
|
||||
{
|
||||
|
||||
2
.github/workflows/scorecard.yml
vendored
2
.github/workflows/scorecard.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
repo_token: ${{ secrets.SCORECARD_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Scorecard results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v4 # tag required by scorecard webapp verification
|
||||
uses: github/codeql-action/upload-sarif@256d634097be96e792d6764f9edaefc4320557b1 # v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: scorecard
|
||||
|
||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [0.2.35] - 2026-03-20
|
||||
|
||||
### Added
|
||||
- **Anonymous read mode** (`NORA_AUTH_ANONYMOUS_READ=true`): allow pull/download without credentials while requiring auth for push. Use case: public demo registries, read-only mirrors.
|
||||
|
||||
### Fixed
|
||||
- Pin slsa-github-generator and codeql-action by SHA instead of tag
|
||||
- Replace anonymous tuple with named struct in activity grouping (readability)
|
||||
- Replace unwrap() with if-let pattern in activity grouping (safety)
|
||||
- Add warning message on SLSA attestation failure instead of silent suppression
|
||||
|
||||
## [0.2.34] - 2026-03-20
|
||||
|
||||
### Fixed
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1277,7 +1277,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nora-registry"
|
||||
version = "0.2.34"
|
||||
version = "0.2.35"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
|
||||
@@ -6,7 +6,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.34"
|
||||
version = "0.2.35"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["DevITWay <devitway@gmail.com>"]
|
||||
|
||||
@@ -94,6 +94,16 @@ pub async fn auth_middleware(
|
||||
return next.run(request).await;
|
||||
}
|
||||
|
||||
// Allow anonymous read if configured
|
||||
let is_read_method = matches!(
|
||||
*request.method(),
|
||||
axum::http::Method::GET | axum::http::Method::HEAD
|
||||
);
|
||||
if state.config.auth.anonymous_read && is_read_method {
|
||||
// Read requests allowed without auth
|
||||
return next.run(request).await;
|
||||
}
|
||||
|
||||
// Extract Authorization header
|
||||
let auth_header = request
|
||||
.headers()
|
||||
|
||||
@@ -200,6 +200,9 @@ fn default_max_file_size() -> u64 {
|
||||
pub struct AuthConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Allow anonymous read access (pull/download without auth, push requires auth)
|
||||
#[serde(default)]
|
||||
pub anonymous_read: bool,
|
||||
#[serde(default = "default_htpasswd_file")]
|
||||
pub htpasswd_file: String,
|
||||
#[serde(default = "default_token_storage")]
|
||||
@@ -279,6 +282,7 @@ impl Default for AuthConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
anonymous_read: false,
|
||||
htpasswd_file: "users.htpasswd".to_string(),
|
||||
token_storage: "data/tokens".to_string(),
|
||||
}
|
||||
@@ -457,6 +461,9 @@ impl Config {
|
||||
if let Ok(val) = env::var("NORA_AUTH_ENABLED") {
|
||||
self.auth.enabled = val.to_lowercase() == "true" || val == "1";
|
||||
}
|
||||
if let Ok(val) = env::var("NORA_AUTH_ANONYMOUS_READ") {
|
||||
self.auth.anonymous_read = val.to_lowercase() == "true" || val == "1";
|
||||
}
|
||||
if let Ok(val) = env::var("NORA_AUTH_HTPASSWD_FILE") {
|
||||
self.auth.htpasswd_file = val;
|
||||
}
|
||||
@@ -741,10 +748,40 @@ mod tests {
|
||||
fn test_auth_config_default() {
|
||||
let a = AuthConfig::default();
|
||||
assert!(!a.enabled);
|
||||
assert!(!a.anonymous_read);
|
||||
assert_eq!(a.htpasswd_file, "users.htpasswd");
|
||||
assert_eq!(a.token_storage, "data/tokens");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_anonymous_read_from_toml() {
|
||||
let toml = r#"
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 4000
|
||||
|
||||
[storage]
|
||||
mode = "local"
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
anonymous_read = true
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert!(config.auth.enabled);
|
||||
assert!(config.auth.anonymous_read);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_override_anonymous_read() {
|
||||
let mut config = Config::default();
|
||||
std::env::set_var("NORA_AUTH_ANONYMOUS_READ", "true");
|
||||
config.apply_env_overrides();
|
||||
assert!(config.auth.anonymous_read);
|
||||
std::env::remove_var("NORA_AUTH_ANONYMOUS_READ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maven_proxy_entry_simple() {
|
||||
let entry = MavenProxyEntry::Simple("https://repo.example.com".to_string());
|
||||
|
||||
@@ -75,43 +75,56 @@ pub fn render_dashboard(data: &DashboardResponse, lang: Lang) -> String {
|
||||
)
|
||||
} else {
|
||||
// Group consecutive identical entries (same action+artifact+registry+source)
|
||||
let mut grouped: Vec<(String, String, String, String, String, usize)> = Vec::new();
|
||||
struct GroupedActivity {
|
||||
time: String,
|
||||
action: String,
|
||||
artifact: String,
|
||||
registry: String,
|
||||
source: String,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
let mut grouped: Vec<GroupedActivity> = Vec::new();
|
||||
for entry in &data.activity {
|
||||
let action = entry.action.to_string();
|
||||
let last_match = grouped
|
||||
.last()
|
||||
.map(|(_, a, art, reg, src, _)| {
|
||||
*a == action
|
||||
&& *art == entry.artifact
|
||||
&& *reg == entry.registry
|
||||
&& *src == entry.source
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let is_repeat = grouped.last().is_some_and(|last| {
|
||||
last.action == action
|
||||
&& last.artifact == entry.artifact
|
||||
&& last.registry == entry.registry
|
||||
&& last.source == entry.source
|
||||
});
|
||||
|
||||
if last_match {
|
||||
grouped.last_mut().unwrap().5 += 1;
|
||||
if is_repeat {
|
||||
if let Some(last) = grouped.last_mut() {
|
||||
last.count += 1;
|
||||
}
|
||||
} else {
|
||||
let time_ago = format_relative_time(&entry.timestamp);
|
||||
grouped.push((
|
||||
time_ago,
|
||||
grouped.push(GroupedActivity {
|
||||
time: format_relative_time(&entry.timestamp),
|
||||
action,
|
||||
entry.artifact.clone(),
|
||||
entry.registry.clone(),
|
||||
entry.source.clone(),
|
||||
1,
|
||||
));
|
||||
artifact: entry.artifact.clone(),
|
||||
registry: entry.registry.clone(),
|
||||
source: entry.source.clone(),
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
grouped
|
||||
.iter()
|
||||
.map(|(time, action, artifact, registry, source, count)| {
|
||||
let display_artifact = if *count > 1 {
|
||||
format!("{} (x{})", artifact, count)
|
||||
.map(|g| {
|
||||
let display_artifact = if g.count > 1 {
|
||||
format!("{} (x{})", g.artifact, g.count)
|
||||
} else {
|
||||
artifact.clone()
|
||||
g.artifact.clone()
|
||||
};
|
||||
render_activity_row(time, action, &display_artifact, registry, source)
|
||||
render_activity_row(
|
||||
&g.time,
|
||||
&g.action,
|
||||
&display_artifact,
|
||||
&g.registry,
|
||||
&g.source,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user