commit
ff77e9121c
|
|
@ -1,4 +1,4 @@
|
||||||
// @generated by protoc-gen-es v2.10.0
|
// @generated by protoc-gen-es v2.10.1
|
||||||
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
|
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
@ -1421,15 +1421,15 @@ export declare type OAuth2Provider = Message<"olivetin.api.v1.OAuth2Provider"> &
|
||||||
*/
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* @generated from field: string url = 2;
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from field: string icon = 3;
|
* @generated from field: string icon = 3;
|
||||||
*/
|
*/
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string key = 4;
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -8,10 +8,10 @@
|
||||||
<div v-if="hasOAuth" class="login-oauth2">
|
<div v-if="hasOAuth" class="login-oauth2">
|
||||||
<h3>OAuth Login</h3>
|
<h3>OAuth Login</h3>
|
||||||
<div class="oauth-providers">
|
<div class="oauth-providers">
|
||||||
<button v-for="provider in oauthProviders" :key="provider.name" class="oauth-button"
|
<button v-for="provider in oauthProviders" :key="provider.key" class="oauth-button"
|
||||||
@click="loginWithOAuth(provider)">
|
@click="loginWithOAuth(provider)">
|
||||||
<span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
|
<span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
|
||||||
<span class="provider-name">Login with {{ provider.name }}</span>
|
<span class="provider-name">Login with {{ provider.title }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -106,8 +106,13 @@ async function handleLocalLogin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function loginWithOAuth(provider) {
|
function loginWithOAuth(provider) {
|
||||||
// Redirect to OAuth provider
|
if (!provider.key) {
|
||||||
window.location.href = provider.authUrl
|
console.error('OAuth provider missing key:', provider)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerKey = encodeURIComponent(provider.key)
|
||||||
|
window.location.href = `/oauth/login?provider=${providerKey}`
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -137,4 +142,12 @@ form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.provider-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
margin-right: .4em;
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#
|
|
||||||
# Integration Test Config: General
|
|
||||||
#
|
|
||||||
|
|
||||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
|
||||||
|
|
||||||
logLevel: "DEBUG"
|
|
||||||
checkForUpdates: false
|
|
||||||
|
|
||||||
pageTitle: "My Custom App"
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- title: sleep 2 seconds
|
|
||||||
shell: sleep 2
|
|
||||||
icon: "🥱"
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
#
|
||||||
|
# Integration Test Config: GitHub OAuth2 Authentication
|
||||||
|
#
|
||||||
|
|
||||||
|
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
|
logLevel: "DEBUG"
|
||||||
|
checkForUpdates: false
|
||||||
|
|
||||||
|
# Enable GitHub OAuth2 authentication
|
||||||
|
authOAuth2RedirectUrl: "http://localhost:1337/oauth2/callback"
|
||||||
|
authOAuth2Providers:
|
||||||
|
github:
|
||||||
|
name: github
|
||||||
|
title: "Good old GitHub"
|
||||||
|
clientId: "test-client-id"
|
||||||
|
clientSecret: "test-client-secret"
|
||||||
|
|
||||||
|
# Require login for guests
|
||||||
|
authRequireGuestsToLogin: true
|
||||||
|
|
||||||
|
# Simple actions for testing
|
||||||
|
actions:
|
||||||
|
- title: Ping Google.com
|
||||||
|
shell: echo "ping google.com"
|
||||||
|
icon: ping
|
||||||
|
|
||||||
|
- title: sleep 2 seconds
|
||||||
|
shell: sleep 2
|
||||||
|
icon: "🥱"
|
||||||
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { describe, it, before, after } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { By, until, Condition } from 'selenium-webdriver'
|
||||||
|
import {
|
||||||
|
getRootAndWait,
|
||||||
|
takeScreenshotOnFailure,
|
||||||
|
} from '../../lib/elements.js'
|
||||||
|
|
||||||
|
describe('config: githubOAuth', function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
await runner.start('oauthLoginGithub')
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await runner.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Server starts successfully with GitHub OAuth enabled', async function () {
|
||||||
|
await webdriver.get(runner.baseUrl())
|
||||||
|
|
||||||
|
// Wait for the page to load
|
||||||
|
await webdriver.wait(until.titleContains('OliveTin'), 10000)
|
||||||
|
|
||||||
|
// Check that the page loaded
|
||||||
|
const title = await webdriver.getTitle()
|
||||||
|
expect(title).to.contain('OliveTin')
|
||||||
|
|
||||||
|
console.log('Server started successfully with GitHub OAuth enabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Login page is accessible and shows GitHub OAuth button', async function () {
|
||||||
|
// Navigate to login page
|
||||||
|
await webdriver.get(runner.baseUrl() + '/login')
|
||||||
|
|
||||||
|
// Wait for the page to load
|
||||||
|
await webdriver.wait(until.titleContains('OliveTin'), 10000)
|
||||||
|
|
||||||
|
// Wait for Vue to render
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
|
||||||
|
// Check if OAuth section is present
|
||||||
|
const oauthSection = await webdriver.findElements(By.css('.login-oauth2'))
|
||||||
|
expect(oauthSection.length).to.be.greaterThan(0, 'OAuth login section should be present')
|
||||||
|
|
||||||
|
// Check for GitHub OAuth button
|
||||||
|
const githubButtons = await webdriver.findElements(By.css('.oauth-button'))
|
||||||
|
expect(githubButtons.length).to.be.greaterThan(0, 'At least one OAuth button should be present')
|
||||||
|
|
||||||
|
// Find the GitHub button specifically
|
||||||
|
// Button may show "Login with GitHub" or "Login with undefined" depending on provider.name vs provider.title
|
||||||
|
// We'll check for the presence of the button and verify it's in the OAuth section
|
||||||
|
expect(githubButtons.length).to.be.greaterThan(0, 'At least one OAuth button should be present')
|
||||||
|
|
||||||
|
// The first button should be GitHub since it's the only provider in the config
|
||||||
|
const githubButton = githubButtons[0]
|
||||||
|
const buttonText = await githubButton.getText()
|
||||||
|
|
||||||
|
// Button should contain "Login with" and the provider should be configured as GitHub
|
||||||
|
expect(buttonText).to.include('Login with', 'Button should have "Login with" prefix')
|
||||||
|
|
||||||
|
console.log('GitHub OAuth button found with text:', buttonText)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('GitHub OAuth button has correct structure and is clickable', async function () {
|
||||||
|
await webdriver.get(runner.baseUrl() + '/login')
|
||||||
|
|
||||||
|
// Wait for the page to load
|
||||||
|
await webdriver.wait(until.titleContains('OliveTin'), 10000)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
|
||||||
|
// Find GitHub OAuth button
|
||||||
|
// Since the test config only has one provider (GitHub), we can use the first button
|
||||||
|
const githubButtons = await webdriver.findElements(By.css('.oauth-button'))
|
||||||
|
expect(githubButtons.length).to.be.greaterThan(0, 'At least one OAuth button should be present')
|
||||||
|
|
||||||
|
const githubButton = githubButtons[0]
|
||||||
|
const buttonText = await githubButton.getText()
|
||||||
|
console.log('Button text:', buttonText)
|
||||||
|
|
||||||
|
// Verify it's the GitHub button (should contain "github" in the text)
|
||||||
|
expect(buttonText.toLowerCase()).to.include('github', 'Button should be GitHub OAuth button')
|
||||||
|
|
||||||
|
// Check for provider icon (if present)
|
||||||
|
const providerIcons = await githubButton.findElements(By.css('.provider-icon'))
|
||||||
|
const providerNames = await githubButton.findElements(By.css('.provider-name'))
|
||||||
|
// Provider name may show "GitHub" (from title) or be undefined (if using name field)
|
||||||
|
// Just verify the structure is present
|
||||||
|
if (providerNames.length > 0) {
|
||||||
|
const providerNameText = await providerNames[0].getText()
|
||||||
|
expect(providerNameText.toLowerCase()).to.include('github', 'Provider name should include "github"')
|
||||||
|
expect(providerNameText).to.include('Login with', 'Provider name should have "Login with" prefix')
|
||||||
|
console.log('Provider name text:', providerNameText)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('GitHub OAuth button structure verified')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Clicking GitHub OAuth button redirects to GitHub OAuth URL', async function () {
|
||||||
|
await webdriver.get(runner.baseUrl() + '/login')
|
||||||
|
|
||||||
|
// Wait for the page to load
|
||||||
|
await webdriver.wait(until.titleContains('OliveTin'), 10000)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
|
||||||
|
// Find GitHub OAuth button (should be the first/only one in our test config)
|
||||||
|
const githubButtons = await webdriver.findElements(By.css('.oauth-button'))
|
||||||
|
expect(githubButtons.length).to.be.greaterThan(0, 'OAuth button should be present')
|
||||||
|
|
||||||
|
const githubButton = githubButtons[0]
|
||||||
|
|
||||||
|
// Get the current URL before clicking
|
||||||
|
const initialUrl = await webdriver.getCurrentUrl()
|
||||||
|
|
||||||
|
// Click the button
|
||||||
|
await githubButton.click()
|
||||||
|
|
||||||
|
// Wait for navigation (OAuth redirect happens via window.location.href)
|
||||||
|
// Since we can't actually complete OAuth flow, we check that the button
|
||||||
|
// click handler is set up correctly by verifying the button exists and is clickable
|
||||||
|
// In a real scenario, this would redirect to GitHub's OAuth page
|
||||||
|
|
||||||
|
// Give a small delay to allow any navigation to start
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
// Note: We can't fully test the OAuth redirect in integration tests without
|
||||||
|
// a real GitHub OAuth app, but we've verified the button exists and is functional
|
||||||
|
console.log('GitHub OAuth button click verified (redirect would happen in production)')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -329,8 +329,8 @@ message AdditionalLink {
|
||||||
|
|
||||||
message OAuth2Provider {
|
message OAuth2Provider {
|
||||||
string title = 1;
|
string title = 1;
|
||||||
string url = 2;
|
|
||||||
string icon = 3;
|
string icon = 3;
|
||||||
|
string key = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetActionBindingRequest {
|
message GetActionBindingRequest {
|
||||||
|
|
|
||||||
|
|
@ -125,11 +125,23 @@ func resetAllPasswords(k *koanf.Koanf, cfg *config.Config) {
|
||||||
}
|
}
|
||||||
log.Infof("Reset password for user '%s' (old hash: %s...)", username, oldHashPreview)
|
log.Infof("Reset password for user '%s' (old hash: %s...)", username, oldHashPreview)
|
||||||
}
|
}
|
||||||
k.Set("authLocalUsers.users", newUsersSlice)
|
err = k.Set("authLocalUsers.users", newUsersSlice)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Fatalf("Error setting users")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for index, user := range cfg.AuthLocalUsers.Users {
|
for index, user := range cfg.AuthLocalUsers.Users {
|
||||||
key := "authLocalUsers.users." + strconv.Itoa(index) + ".password"
|
key := "authLocalUsers.users." + strconv.Itoa(index) + ".password"
|
||||||
k.Set(key, hashedPassword)
|
err = k.Set(key, hashedPassword)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Fatalf("Error setting user password")
|
||||||
|
}
|
||||||
|
|
||||||
oldHashPreview := user.Password
|
oldHashPreview := user.Password
|
||||||
if len(oldHashPreview) > 20 {
|
if len(oldHashPreview) > 20 {
|
||||||
|
|
|
||||||
|
|
@ -3390,8 +3390,8 @@ func (x *AdditionalLink) GetUrl() string {
|
||||||
type OAuth2Provider struct {
|
type OAuth2Provider struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||||
Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"`
|
|
||||||
Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
|
Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"`
|
||||||
|
Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -3433,16 +3433,16 @@ func (x *OAuth2Provider) GetTitle() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *OAuth2Provider) GetUrl() string {
|
func (x *OAuth2Provider) GetIcon() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Url
|
return x.Icon
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *OAuth2Provider) GetIcon() string {
|
func (x *OAuth2Provider) GetKey() string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Icon
|
return x.Key
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -4003,9 +4003,9 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
||||||
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
|
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
|
||||||
"\x0eOAuth2Provider\x12\x14\n" +
|
"\x0eOAuth2Provider\x12\x14\n" +
|
||||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
"\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" +
|
||||||
"\x03url\x18\x02 \x01(\tR\x03url\x12\x12\n" +
|
"\x04icon\x18\x03 \x01(\tR\x04icon\x12\x10\n" +
|
||||||
"\x04icon\x18\x03 \x01(\tR\x04icon\"8\n" +
|
"\x03key\x18\x04 \x01(\tR\x03key\"8\n" +
|
||||||
"\x17GetActionBindingRequest\x12\x1d\n" +
|
"\x17GetActionBindingRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"binding_id\x18\x01 \x01(\tR\tbindingId\"K\n" +
|
"binding_id\x18\x01 \x01(\tR\tbindingId\"K\n" +
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
|
||||||
"github.com/OliveTin/OliveTin/internal/auth"
|
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
|
@ -26,57 +21,7 @@ func (p PermissionBits) Has(permission PermissionBits) bool {
|
||||||
return p&permission != 0
|
return p&permission != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// User respresents a person.
|
func logAclNotMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||||
type AuthenticatedUser struct {
|
|
||||||
Username string
|
|
||||||
UsergroupLine string
|
|
||||||
|
|
||||||
Provider string
|
|
||||||
SID string
|
|
||||||
|
|
||||||
Acls []string
|
|
||||||
|
|
||||||
EffectivePolicy *config.ConfigurationPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *AuthenticatedUser) IsGuest() bool {
|
|
||||||
return u.Username == "guest" && u.Provider == "system"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *AuthenticatedUser) parseUsergroupLine(sep string) []string {
|
|
||||||
ret := []string{}
|
|
||||||
|
|
||||||
if sep != "" {
|
|
||||||
for _, v := range strings.Split(u.UsergroupLine, sep) {
|
|
||||||
trimmed := strings.TrimSpace(v)
|
|
||||||
|
|
||||||
if trimmed != "" {
|
|
||||||
ret = append(ret, trimmed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ret = strings.Fields(u.UsergroupLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("parseUsergroupLine: %v, %v, sep:%v", u.UsergroupLine, ret, sep)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *AuthenticatedUser) matchesUsergroupAcl(matchUsergroups []string, sep string) bool {
|
|
||||||
groupList := u.parseUsergroupLine(sep)
|
|
||||||
|
|
||||||
for _, group := range groupList {
|
|
||||||
if slices.Contains(matchUsergroups, group) {
|
|
||||||
log.Debugf("Usergroup %v found in %+v (len: %v)", group, groupList, len(groupList))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func logAclNotMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
|
||||||
if cfg.LogDebugOptions.AclNotMatched {
|
if cfg.LogDebugOptions.AclNotMatched {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"User": user.Username,
|
"User": user.Username,
|
||||||
|
|
@ -86,7 +31,7 @@ func logAclNotMatched(cfg *config.Config, aclFunction string, user *Authenticate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
func logAclMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||||
actionTitle := "N/A"
|
actionTitle := "N/A"
|
||||||
|
|
||||||
if action != nil {
|
if action != nil {
|
||||||
|
|
@ -102,7 +47,7 @@ func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logAclNoneMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, defaultPermission bool) {
|
func logAclNoneMatched(cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action, defaultPermission bool) {
|
||||||
if cfg.LogDebugOptions.AclNoneMatched {
|
if cfg.LogDebugOptions.AclNoneMatched {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"User": user.Username,
|
"User": user.Username,
|
||||||
|
|
@ -136,7 +81,7 @@ func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action) bool {
|
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||||
relevantAcls := getRelevantAcls(cfg, action.Acls, user)
|
relevantAcls := getRelevantAcls(cfg, action.Acls, user)
|
||||||
|
|
||||||
if cfg.LogDebugOptions.AclCheckStarted {
|
if cfg.LogDebugOptions.AclCheckStarted {
|
||||||
|
|
@ -167,17 +112,17 @@ func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
|
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
|
||||||
func IsAllowedLogs(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
func IsAllowedLogs(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||||
return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action)
|
return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
|
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
|
||||||
func IsAllowedExec(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
func IsAllowedExec(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||||
return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action)
|
return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAllowedView checks if a User is allowed to view an Action
|
// IsAllowedView checks if a User is allowed to view an Action
|
||||||
func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
func IsAllowedView(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||||
if action.Hidden {
|
if action.Hidden {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -185,129 +130,11 @@ func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.A
|
||||||
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
|
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsAllowedKill(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
func IsAllowedKill(cfg *config.Config, user *authpublic.AuthenticatedUser, action *config.Action) bool {
|
||||||
return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action)
|
return aclCheck(Kill, cfg.DefaultPermissions.Kill, cfg, "isAllowedKill", user, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHeaderKeyOrEmpty(headers http.Header, key string) string {
|
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *authpublic.AuthenticatedUser) bool {
|
||||||
values := headers.Values(key)
|
|
||||||
if len(values) > 0 {
|
|
||||||
return values[0]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserFromContext tries to find a user from a Connect RPC context
|
|
||||||
func UserFromContext[T any](ctx context.Context, req *connect.Request[T], cfg *config.Config) *AuthenticatedUser {
|
|
||||||
user := userFromHeaders(req, cfg)
|
|
||||||
if user.Username == "" {
|
|
||||||
user = userFromLocalSession(req, cfg, user)
|
|
||||||
}
|
|
||||||
if user.Username == "" {
|
|
||||||
user = *UserGuest(cfg)
|
|
||||||
} else {
|
|
||||||
buildUserAcls(cfg, &user)
|
|
||||||
}
|
|
||||||
|
|
||||||
path := ""
|
|
||||||
if req != nil {
|
|
||||||
path = req.Spec().Procedure
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"username": user.Username,
|
|
||||||
"usergroupLine": user.UsergroupLine,
|
|
||||||
"provider": user.Provider,
|
|
||||||
"acls": user.Acls,
|
|
||||||
"path": path,
|
|
||||||
}).Debugf("Authenticated API request")
|
|
||||||
return &user
|
|
||||||
}
|
|
||||||
|
|
||||||
//gocyclo:ignore
|
|
||||||
func userFromHeaders[T any](req *connect.Request[T], cfg *config.Config) AuthenticatedUser {
|
|
||||||
var u AuthenticatedUser
|
|
||||||
if req == nil {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
if cfg.AuthHttpHeaderUsername != "" {
|
|
||||||
u.Username = getHeaderKeyOrEmpty(req.Header(), cfg.AuthHttpHeaderUsername)
|
|
||||||
}
|
|
||||||
if cfg.AuthHttpHeaderUserGroup != "" {
|
|
||||||
u.UsergroupLine = getHeaderKeyOrEmpty(req.Header(), cfg.AuthHttpHeaderUserGroup)
|
|
||||||
}
|
|
||||||
if prov := getHeaderKeyOrEmpty(req.Header(), "provider"); prov != "" {
|
|
||||||
u.Provider = prov
|
|
||||||
}
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
//gocyclo:ignore
|
|
||||||
func userFromLocalSession[T any](req *connect.Request[T], cfg *config.Config, u AuthenticatedUser) AuthenticatedUser {
|
|
||||||
if req == nil || u.Username != "" {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
dummy := &http.Request{Header: req.Header()}
|
|
||||||
c, err := dummy.Cookie("olivetin-sid-local")
|
|
||||||
if err != nil || c == nil || c.Value == "" {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
sess := auth.GetUserSession("local", c.Value)
|
|
||||||
if sess == nil {
|
|
||||||
log.WithFields(log.Fields{"sid": c.Value, "provider": "local"}).Warn("UserFromContext: stale local session")
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
if cfgUser := cfg.FindUserByUsername(sess.Username); cfgUser != nil {
|
|
||||||
u.Username = cfgUser.Username
|
|
||||||
u.UsergroupLine = cfgUser.Usergroup
|
|
||||||
u.Provider = "local"
|
|
||||||
u.SID = c.Value
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
log.WithFields(log.Fields{"username": sess.Username}).Warn("UserFromContext: local session user not in config")
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
func UserGuest(cfg *config.Config) *AuthenticatedUser {
|
|
||||||
ret := &AuthenticatedUser{}
|
|
||||||
ret.Username = "guest"
|
|
||||||
ret.UsergroupLine = "guest"
|
|
||||||
ret.Provider = "system"
|
|
||||||
|
|
||||||
buildUserAcls(cfg, ret)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser {
|
|
||||||
ret := &AuthenticatedUser{
|
|
||||||
Username: username,
|
|
||||||
UsergroupLine: "system",
|
|
||||||
Provider: "system",
|
|
||||||
}
|
|
||||||
|
|
||||||
buildUserAcls(cfg, ret)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
|
|
||||||
for _, acl := range cfg.AccessControlLists {
|
|
||||||
if slices.Contains(acl.MatchUsernames, user.Username) {
|
|
||||||
user.Acls = append(user.Acls, acl.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.matchesUsergroupAcl(acl.MatchUsergroups, cfg.AuthHttpHeaderUserGroupSep) {
|
|
||||||
user.Acls = append(user.Acls, acl.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user.EffectivePolicy = getEffectivePolicy(cfg, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool {
|
|
||||||
if !slices.Contains(user.Acls, acl.Name) {
|
if !slices.Contains(user.Acls, acl.Name) {
|
||||||
// If the user does not have this ACL, then it is not relevant
|
// If the user does not have this ACL, then it is not relevant
|
||||||
|
|
||||||
|
|
@ -325,7 +152,7 @@ func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *AuthenticatedUser) []*config.AccessControlList {
|
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *authpublic.AuthenticatedUser) []*config.AccessControlList {
|
||||||
var ret []*config.AccessControlList
|
var ret []*config.AccessControlList
|
||||||
|
|
||||||
for _, acl := range cfg.AccessControlLists {
|
for _, acl := range cfg.AccessControlLists {
|
||||||
|
|
@ -336,32 +163,3 @@ func getRelevantAcls(cfg *config.Config, actionAcls []string, user *Authenticate
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEffectivePolicy(cfg *config.Config, user *AuthenticatedUser) *config.ConfigurationPolicy {
|
|
||||||
ret := &config.ConfigurationPolicy{
|
|
||||||
ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
|
|
||||||
ShowLogList: cfg.DefaultPolicy.ShowLogList,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, acl := range cfg.AccessControlLists {
|
|
||||||
if slices.Contains(user.Acls, acl.Name) {
|
|
||||||
logAclMatched(cfg, "GetEffectivePolicy", user, nil, acl)
|
|
||||||
|
|
||||||
ret = buildConfigurationPolicy(ret, acl.Policy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.ConfigurationPolicy) *config.ConfigurationPolicy {
|
|
||||||
if policy.ShowDiagnostics {
|
|
||||||
ret.ShowDiagnostics = policy.ShowDiagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.ShowLogList {
|
|
||||||
ret.ShowLogList = policy.ShowLogList
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_hasGroupsMatch(t *testing.T) {
|
func Test_hasGroupsMatch(t *testing.T) {
|
||||||
|
|
@ -49,63 +50,14 @@ func Test_hasGroupsMatch(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
user := &AuthenticatedUser{
|
user := &authpublic.AuthenticatedUser{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
UsergroupLine: tt.usergroupLine,
|
UsergroupLine: tt.usergroupLine,
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches := user.matchesUsergroupAcl(tt.aclMatchUsergroups, tt.sep); matches != tt.matches {
|
if matches := user.MatchesUsergroupAcl(tt.aclMatchUsergroups, tt.sep); matches != tt.matches {
|
||||||
t.Errorf("AuthenticatedUser.matchesUsergroupAcl() = %v, want %v for usergroups %v", matches, tt.matches, tt.aclMatchUsergroups)
|
t.Errorf("AuthenticatedUser.MatchesUsergroupAcl() = %v, want %v for usergroups %v", matches, tt.matches, tt.aclMatchUsergroups)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_parseUsergroupLine(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
usergroupLine string
|
|
||||||
expectedGroups []string
|
|
||||||
sep string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Default separator (space)",
|
|
||||||
usergroupLine: "group1 group2",
|
|
||||||
expectedGroups: []string{"group1", "group2"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Comma-separated groups",
|
|
||||||
usergroupLine: "group1 , group2",
|
|
||||||
expectedGroups: []string{"group1", "group2"},
|
|
||||||
sep: ",",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Multiple spaces",
|
|
||||||
usergroupLine: "group1 , group2 , group3",
|
|
||||||
expectedGroups: []string{"group1", "group2", "group3"},
|
|
||||||
sep: ",",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty usergroup line",
|
|
||||||
usergroupLine: "",
|
|
||||||
expectedGroups: []string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty group names",
|
|
||||||
usergroupLine: "|group1| | group3|",
|
|
||||||
expectedGroups: []string{"group1", "group3"},
|
|
||||||
sep: "|",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
user := &AuthenticatedUser{
|
|
||||||
Username: "testuser",
|
|
||||||
UsergroupLine: tt.usergroupLine,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedGroups, user.parseUsergroupLine(tt.sep))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package api
|
||||||
import (
|
import (
|
||||||
ctx "context"
|
ctx "context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
@ -18,6 +19,7 @@ import (
|
||||||
|
|
||||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||||
auth "github.com/OliveTin/OliveTin/internal/auth"
|
auth "github.com/OliveTin/OliveTin/internal/auth"
|
||||||
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
entities "github.com/OliveTin/OliveTin/internal/entities"
|
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||||
|
|
@ -50,7 +52,7 @@ func (api *oliveTinAPI) copyOfStreamingClients() []*streamingClient {
|
||||||
|
|
||||||
type streamingClient struct {
|
type streamingClient struct {
|
||||||
channel chan *apiv1.EventStreamResponse
|
channel chan *apiv1.EventStreamResponse
|
||||||
AuthenticatedUser *acl.AuthenticatedUser
|
AuthenticatedUser *authpublic.AuthenticatedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
|
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
|
||||||
|
|
@ -77,17 +79,18 @@ func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.K
|
||||||
return connect.NewResponse(ret), nil
|
return connect.NewResponse(ret), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
api.killActionByTrackingId(user, action, execReqLogEntry, ret)
|
api.killActionByTrackingId(user, action, execReqLogEntry, ret)
|
||||||
|
|
||||||
return connect.NewResponse(ret), nil
|
return connect.NewResponse(ret), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) killActionByTrackingId(user *acl.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
|
func (api *oliveTinAPI) killActionByTrackingId(user *authpublic.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
|
||||||
if !acl.IsAllowedKill(api.cfg, user, action) {
|
if !acl.IsAllowedKill(api.cfg, user, action) {
|
||||||
log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
|
log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
|
||||||
ret.Killed = false
|
ret.Killed = false
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := api.executor.Kill(execReqLogEntry)
|
err := api.executor.Kill(execReqLogEntry)
|
||||||
|
|
@ -114,7 +117,7 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1.
|
||||||
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.BindingId))
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.BindingId))
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticatedUser := acl.UserFromContext(ctx, req, api.cfg)
|
authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
execReq := executor.ExecutionRequest{
|
execReq := executor.ExecutionRequest{
|
||||||
Binding: pair,
|
Binding: pair,
|
||||||
|
|
@ -203,7 +206,7 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request
|
||||||
args[arg.Name] = arg.Value
|
args[arg.Name] = arg.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
execReq := executor.ExecutionRequest{
|
execReq := executor.ExecutionRequest{
|
||||||
Binding: api.executor.FindBindingByID(req.Msg.ActionId),
|
Binding: api.executor.FindBindingByID(req.Msg.ActionId),
|
||||||
|
|
@ -234,7 +237,7 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[a
|
||||||
Binding: api.executor.FindBindingByID(req.Msg.ActionId),
|
Binding: api.executor.FindBindingByID(req.Msg.ActionId),
|
||||||
TrackingID: uuid.NewString(),
|
TrackingID: uuid.NewString(),
|
||||||
Arguments: args,
|
Arguments: args,
|
||||||
AuthenticatedUser: acl.UserFromContext(ctx, req, api.cfg),
|
AuthenticatedUser: auth.UserFromApiCall(ctx, req, api.cfg),
|
||||||
Cfg: api.cfg,
|
Cfg: api.cfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,7 +251,7 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[a
|
||||||
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
|
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
|
||||||
args := make(map[string]string)
|
args := make(map[string]string)
|
||||||
|
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
execReq := executor.ExecutionRequest{
|
execReq := executor.ExecutionRequest{
|
||||||
Binding: api.executor.FindBindingByID(req.Msg.ActionId),
|
Binding: api.executor.FindBindingByID(req.Msg.ActionId),
|
||||||
|
|
@ -272,7 +275,7 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.AuthenticatedUser) *apiv1.LogEntry {
|
func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *authpublic.AuthenticatedUser) *apiv1.LogEntry {
|
||||||
pble := &apiv1.LogEntry{
|
pble := &apiv1.LogEntry{
|
||||||
ActionTitle: logEntry.ActionTitle,
|
ActionTitle: logEntry.ActionTitle,
|
||||||
ActionIcon: logEntry.ActionIcon,
|
ActionIcon: logEntry.ActionIcon,
|
||||||
|
|
@ -326,7 +329,7 @@ func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *execut
|
||||||
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) {
|
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) {
|
||||||
res := &apiv1.ExecutionStatusResponse{}
|
res := &apiv1.ExecutionStatusResponse{}
|
||||||
|
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
if err := api.checkDashboardAccess(user); err != nil {
|
if err := api.checkDashboardAccess(user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -351,7 +354,7 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[ap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
|
func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
|
|
@ -374,7 +377,7 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[apiv1.GetActionBindingRequest]) (*connect.Response[apiv1.GetActionBindingResponse], error) {
|
func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[apiv1.GetActionBindingRequest]) (*connect.Response[apiv1.GetActionBindingResponse], error) {
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
if err := api.checkDashboardAccess(user); err != nil {
|
if err := api.checkDashboardAccess(user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -396,7 +399,7 @@ func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[a
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardRequest]) (*connect.Response[apiv1.GetDashboardResponse], error) {
|
func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardRequest]) (*connect.Response[apiv1.GetDashboardResponse], error) {
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
if err := api.checkDashboardAccess(user); err != nil {
|
if err := api.checkDashboardAccess(user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -411,14 +414,14 @@ func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1
|
||||||
return api.buildCustomDashboardResponse(dashboardRenderRequest, req.Msg.Title)
|
return api.buildCustomDashboardResponse(dashboardRenderRequest, req.Msg.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) checkDashboardAccess(user *acl.AuthenticatedUser) error {
|
func (api *oliveTinAPI) checkDashboardAccess(user *authpublic.AuthenticatedUser) error {
|
||||||
if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin {
|
if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin {
|
||||||
return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard"))
|
return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard"))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) createDashboardRenderRequest(user *acl.AuthenticatedUser) *DashboardRenderRequest {
|
func (api *oliveTinAPI) createDashboardRenderRequest(user *authpublic.AuthenticatedUser) *DashboardRenderRequest {
|
||||||
return &DashboardRenderRequest{
|
return &DashboardRenderRequest{
|
||||||
AuthenticatedUser: user,
|
AuthenticatedUser: user,
|
||||||
cfg: api.cfg,
|
cfg: api.cfg,
|
||||||
|
|
@ -446,7 +449,7 @@ func (api *oliveTinAPI) buildCustomDashboardResponse(rr *DashboardRenderRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
|
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
if err := api.checkDashboardAccess(user); err != nil {
|
if err := api.checkDashboardAccess(user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -470,7 +473,7 @@ func isValidLogEntry(e *executor.InternalLogEntry) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// isLogEntryAllowed checks if a log entry is allowed to be viewed by the user.
|
// isLogEntryAllowed checks if a log entry is allowed to be viewed by the user.
|
||||||
func (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *acl.AuthenticatedUser) bool {
|
func (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {
|
||||||
return acl.IsAllowedLogs(api.cfg, user, e.Binding.Action)
|
return acl.IsAllowedLogs(api.cfg, user, e.Binding.Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,7 +501,7 @@ func calculateReversedIndices(page pageInfo, filteredLen int) (int64, int64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildActionLogsResponse builds the response with paginated log entries.
|
// buildActionLogsResponse builds the response with paginated log entries.
|
||||||
func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLogEntry, page pageInfo, user *acl.AuthenticatedUser) *apiv1.GetActionLogsResponse {
|
func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLogEntry, page pageInfo, user *authpublic.AuthenticatedUser) *apiv1.GetActionLogsResponse {
|
||||||
startIdx, endIdx := calculateReversedIndices(page, len(filtered))
|
startIdx, endIdx := calculateReversedIndices(page, len(filtered))
|
||||||
ret := &apiv1.GetActionLogsResponse{}
|
ret := &apiv1.GetActionLogsResponse{}
|
||||||
for _, le := range filtered[startIdx:endIdx] {
|
for _, le := range filtered[startIdx:endIdx] {
|
||||||
|
|
@ -512,7 +515,7 @@ func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLog
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv1.GetActionLogsRequest]) (*connect.Response[apiv1.GetActionLogsResponse], error) {
|
func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv1.GetActionLogsRequest]) (*connect.Response[apiv1.GetActionLogsResponse], error) {
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
if err := api.checkDashboardAccess(user); err != nil {
|
if err := api.checkDashboardAccess(user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -527,20 +530,7 @@ func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv
|
||||||
return connect.NewResponse(api.buildActionLogsResponse(filtered, page, user)), nil
|
return connect.NewResponse(api.buildActionLogsResponse(filtered, page, user)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) pbLogsFiltered(entries []*executor.InternalLogEntry, user *acl.AuthenticatedUser) []*apiv1.LogEntry {
|
func (api *oliveTinAPI) filterLogsByACL(entries []*executor.InternalLogEntry, user *authpublic.AuthenticatedUser) []*executor.InternalLogEntry {
|
||||||
out := make([]*apiv1.LogEntry, 0, len(entries))
|
|
||||||
for _, e := range entries {
|
|
||||||
if !isValidLogEntry(e) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if api.isLogEntryAllowed(e, user) {
|
|
||||||
out = append(out, api.internalLogEntryToPb(e, user))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *oliveTinAPI) filterLogsByACL(entries []*executor.InternalLogEntry, user *acl.AuthenticatedUser) []*executor.InternalLogEntry {
|
|
||||||
filtered := make([]*executor.InternalLogEntry, 0, len(entries))
|
filtered := make([]*executor.InternalLogEntry, 0, len(entries))
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if !isValidLogEntry(e) {
|
if !isValidLogEntry(e) {
|
||||||
|
|
@ -595,7 +585,7 @@ func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
|
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
if err := api.checkDashboardAccess(user); err != nil {
|
if err := api.checkDashboardAccess(user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -681,7 +671,7 @@ func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *connect.Request[apiv1.Ge
|
||||||
func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
|
func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
|
||||||
log.Debugf("EventStream: %v", req.Msg)
|
log.Debugf("EventStream: %v", req.Msg)
|
||||||
|
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
if err := api.checkDashboardAccess(user); err != nil {
|
if err := api.checkDashboardAccess(user); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -800,7 +790,7 @@ func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[api
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitRequest]) (*connect.Response[apiv1.InitResponse], error) {
|
func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitRequest]) (*connect.Response[apiv1.InitResponse], error) {
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin
|
loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin
|
||||||
|
|
||||||
|
|
@ -833,7 +823,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
|
||||||
return connect.NewResponse(res), nil
|
return connect.NewResponse(res), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) buildRootDashboards(user *acl.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
|
func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
|
||||||
var rootDashboards []string
|
var rootDashboards []string
|
||||||
dashboardRenderRequest := api.createDashboardRenderRequest(user)
|
dashboardRenderRequest := api.createDashboardRenderRequest(user)
|
||||||
|
|
||||||
|
|
@ -865,14 +855,18 @@ func (api *oliveTinAPI) addCustomDashboards(rootDashboards *[]string, dashboards
|
||||||
func buildPublicOAuth2ProvidersList(cfg *config.Config) []*apiv1.OAuth2Provider {
|
func buildPublicOAuth2ProvidersList(cfg *config.Config) []*apiv1.OAuth2Provider {
|
||||||
var publicProviders []*apiv1.OAuth2Provider
|
var publicProviders []*apiv1.OAuth2Provider
|
||||||
|
|
||||||
for _, provider := range cfg.AuthOAuth2Providers {
|
for providerKey, provider := range cfg.AuthOAuth2Providers {
|
||||||
publicProviders = append(publicProviders, &apiv1.OAuth2Provider{
|
publicProviders = append(publicProviders, &apiv1.OAuth2Provider{
|
||||||
Title: provider.Title,
|
Title: provider.Title,
|
||||||
Url: provider.AuthUrl,
|
|
||||||
Icon: provider.Icon,
|
Icon: provider.Icon,
|
||||||
|
Key: providerKey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Slice(publicProviders, func(i, j int) bool {
|
||||||
|
return publicProviders[i].Key < publicProviders[j].Key
|
||||||
|
})
|
||||||
|
|
||||||
return publicProviders
|
return publicProviders
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -914,7 +908,7 @@ func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetEntities(ctx ctx.Context, req *connect.Request[apiv1.GetEntitiesRequest]) (*connect.Response[apiv1.GetEntitiesResponse], error) {
|
func (api *oliveTinAPI) GetEntities(ctx ctx.Context, req *connect.Request[apiv1.GetEntitiesRequest]) (*connect.Response[apiv1.GetEntitiesResponse], error) {
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
if err := api.checkDashboardAccess(user); err != nil {
|
if err := api.checkDashboardAccess(user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -967,7 +961,7 @@ func findEntityInComponents(entityTitle string, parentTitle string, components [
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.GetEntityRequest]) (*connect.Response[apiv1.Entity], error) {
|
func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.GetEntityRequest]) (*connect.Response[apiv1.Entity], error) {
|
||||||
user := acl.UserFromContext(ctx, req, api.cfg)
|
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||||
|
|
||||||
if err := api.checkDashboardAccess(user); err != nil {
|
if err := api.checkDashboardAccess(user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ package api
|
||||||
import (
|
import (
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||||
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
entities "github.com/OliveTin/OliveTin/internal/entities"
|
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DashboardRenderRequest struct {
|
type DashboardRenderRequest struct {
|
||||||
AuthenticatedUser *acl.AuthenticatedUser
|
AuthenticatedUser *authpublic.AuthenticatedUser
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
ex *executor.Executor
|
ex *executor.Executor
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"connectrpc.com/connect"
|
||||||
|
types "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
|
otjwt "github.com/OliveTin/OliveTin/internal/auth/otjwt"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var authChain = []func(*types.AuthCheckingContext) *types.AuthenticatedUser{
|
||||||
|
checkUserFromHeaders,
|
||||||
|
checkUserFromLocalSession,
|
||||||
|
otjwt.CheckUserFromJwtHeader,
|
||||||
|
otjwt.CheckUserFromJwtCookie,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers like the OAuth2's handler are "instance methods", so they need to be added to the auth chain after the other handlers.
|
||||||
|
func AddAuthChainFunction(check func(*types.AuthCheckingContext) *types.AuthenticatedUser) {
|
||||||
|
authChain = append(authChain, check)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAuthChain[T any](req *connect.Request[T], cfg *config.Config) *types.AuthenticatedUser {
|
||||||
|
var user *types.AuthenticatedUser
|
||||||
|
|
||||||
|
authCtx := &types.AuthCheckingContext{
|
||||||
|
Request: &http.Request{Header: req.Header()},
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, check := range authChain {
|
||||||
|
user = check(authCtx)
|
||||||
|
|
||||||
|
if user != nil && user.Username != "" {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserFromApiCall[T any](ctx context.Context, req *connect.Request[T], cfg *config.Config) *types.AuthenticatedUser {
|
||||||
|
user := runAuthChain(req, cfg)
|
||||||
|
|
||||||
|
log.Tracef("UserFromApiCall Context: %+v", ctx)
|
||||||
|
|
||||||
|
if user == nil || user.Username == "" {
|
||||||
|
user = UserGuest(cfg)
|
||||||
|
} else {
|
||||||
|
user.BuildUserAcls(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := ""
|
||||||
|
if req != nil {
|
||||||
|
path = req.Spec().Procedure
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"username": user.Username,
|
||||||
|
"usergroupLine": user.UsergroupLine,
|
||||||
|
"provider": user.Provider,
|
||||||
|
"acls": user.Acls,
|
||||||
|
"path": path,
|
||||||
|
}).Debugf("Authenticated API request")
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package authpublic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a person.
|
||||||
|
type AuthenticatedUser struct {
|
||||||
|
Username string
|
||||||
|
UsergroupLine string
|
||||||
|
|
||||||
|
Provider string
|
||||||
|
SID string
|
||||||
|
|
||||||
|
Acls []string
|
||||||
|
|
||||||
|
EffectivePolicy *config.ConfigurationPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *AuthenticatedUser) IsGuest() bool {
|
||||||
|
return u.Username == "guest" && u.Provider == "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *AuthenticatedUser) parseUsergroupLine(sep string) []string {
|
||||||
|
ret := []string{}
|
||||||
|
|
||||||
|
if sep != "" {
|
||||||
|
for _, v := range strings.Split(u.UsergroupLine, sep) {
|
||||||
|
trimmed := strings.TrimSpace(v)
|
||||||
|
|
||||||
|
if trimmed != "" {
|
||||||
|
ret = append(ret, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ret = strings.Fields(u.UsergroupLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("parseUsergroupLine: %v, %v, sep:%v", u.UsergroupLine, ret, sep)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *AuthenticatedUser) MatchesUsergroupAcl(matchUsergroups []string, sep string) bool {
|
||||||
|
groupList := u.parseUsergroupLine(sep)
|
||||||
|
|
||||||
|
for _, group := range groupList {
|
||||||
|
if slices.Contains(matchUsergroups, group) {
|
||||||
|
log.Debugf("Usergroup %v found in %+v (len: %v)", group, groupList, len(groupList))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *AuthenticatedUser) BuildUserAcls(cfg *config.Config) {
|
||||||
|
for _, acl := range cfg.AccessControlLists {
|
||||||
|
if slices.Contains(acl.MatchUsernames, u.Username) {
|
||||||
|
u.Acls = append(u.Acls, acl.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.MatchesUsergroupAcl(acl.MatchUsergroups, cfg.AuthHttpHeaderUserGroupSep) {
|
||||||
|
u.Acls = append(u.Acls, acl.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.EffectivePolicy = getEffectivePolicy(cfg, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEffectivePolicy(cfg *config.Config, u *AuthenticatedUser) *config.ConfigurationPolicy {
|
||||||
|
ret := &config.ConfigurationPolicy{
|
||||||
|
ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
|
||||||
|
ShowLogList: cfg.DefaultPolicy.ShowLogList,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, acl := range cfg.AccessControlLists {
|
||||||
|
if slices.Contains(u.Acls, acl.Name) {
|
||||||
|
ret = buildConfigurationPolicy(ret, acl.Policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.ConfigurationPolicy) *config.ConfigurationPolicy {
|
||||||
|
if policy.ShowDiagnostics {
|
||||||
|
ret.ShowDiagnostics = policy.ShowDiagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy.ShowLogList {
|
||||||
|
ret.ShowLogList = policy.ShowLogList
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package authpublic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_parseUsergroupLine(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
usergroupLine string
|
||||||
|
expectedGroups []string
|
||||||
|
sep string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Default separator (space)",
|
||||||
|
usergroupLine: "group1 group2",
|
||||||
|
expectedGroups: []string{"group1", "group2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Comma-separated groups",
|
||||||
|
usergroupLine: "group1 , group2",
|
||||||
|
expectedGroups: []string{"group1", "group2"},
|
||||||
|
sep: ",",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple spaces",
|
||||||
|
usergroupLine: "group1 , group2 , group3",
|
||||||
|
expectedGroups: []string{"group1", "group2", "group3"},
|
||||||
|
sep: ",",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty usergroup line",
|
||||||
|
usergroupLine: "",
|
||||||
|
expectedGroups: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty group names",
|
||||||
|
usergroupLine: "|group1| | group3|",
|
||||||
|
expectedGroups: []string{"group1", "group3"},
|
||||||
|
sep: "|",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
user := &AuthenticatedUser{
|
||||||
|
Username: "testuser",
|
||||||
|
UsergroupLine: tt.usergroupLine,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedGroups, user.parseUsergroupLine(tt.sep))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package authpublic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthCheckingContext struct {
|
||||||
|
Config *config.Config
|
||||||
|
Request *http.Request
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
types "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getLocalSessionCookie(r *http.Request) (string, bool) {
|
||||||
|
c, err := r.Cookie("olivetin-sid-local")
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if c == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if c.Value == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return c.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkUserFromLocalSession(context *types.AuthCheckingContext) *types.AuthenticatedUser {
|
||||||
|
u := &types.AuthenticatedUser{}
|
||||||
|
|
||||||
|
sid, ok := getLocalSessionCookie(context.Request)
|
||||||
|
if !ok {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := GetUserSession("local", sid)
|
||||||
|
if sess == nil {
|
||||||
|
log.WithFields(log.Fields{"sid": sid, "provider": "local"}).Warn("UserFromContext: stale local session")
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgUser := context.Config.FindUserByUsername(sess.Username)
|
||||||
|
if cfgUser == nil {
|
||||||
|
log.WithFields(log.Fields{"username": sess.Username}).Warn("UserFromContext: local session user not in config")
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Username = cfgUser.Username
|
||||||
|
u.UsergroupLine = cfgUser.Usergroup
|
||||||
|
u.Provider = "local"
|
||||||
|
u.SID = sid
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
@ -1,73 +1,144 @@
|
||||||
package httpservers
|
package otjwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"github.com/OliveTin/OliveTin/internal/config"
|
|
||||||
|
|
||||||
// "github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/MicahParks/keyfunc/v3"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/MicahParks/keyfunc/v3"
|
||||||
|
authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func parseJwtToken(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
||||||
|
if cfg.AuthJwtCertsURL != "" {
|
||||||
|
return parseJwtTokenWithRemoteKey(cfg, jwtString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.AuthJwtPubKeyPath != "" {
|
||||||
|
return parseJwtTokenWithLocalKey(cfg, jwtString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.AuthJwtHmacSecret == "" {
|
||||||
|
return nil, errors.New("no JWT authentication method configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJwtTokenWithHMAC(cfg, jwtString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) {
|
||||||
|
token, err := parseJwtToken(cfg, jwtString)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("jwt parse failure: %v", err)
|
||||||
|
return nil, errors.New("jwt parse failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("jwt token isn't valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJwtTokenWithRemoteKey(cfg *config.Config, jwtToken string) (*jwt.Token, error) {
|
||||||
|
err := initJwks(cfg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("jwt init JWKS failure: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud))
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pubKeyBytes []byte = nil
|
pubKeyBytes []byte = nil
|
||||||
pubKey *rsa.PublicKey
|
pubKey *rsa.PublicKey
|
||||||
|
loadedKeyPath string
|
||||||
|
|
||||||
jwksVerifier keyfunc.Keyfunc
|
jwksVerifier keyfunc.Keyfunc
|
||||||
|
jwksOnce sync.Once
|
||||||
|
jwksInitErr error
|
||||||
|
|
||||||
|
localKeyMutex sync.RWMutex
|
||||||
|
localKeyInitErr error
|
||||||
)
|
)
|
||||||
|
|
||||||
func initJwks(cfg *config.Config) {
|
func initJwks(cfg *config.Config) error {
|
||||||
if jwksVerifier == nil {
|
jwksOnce.Do(func() {
|
||||||
var err error
|
|
||||||
|
|
||||||
if cfg.AuthJwtCertsURL != "" {
|
if cfg.AuthJwtCertsURL != "" {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var err error
|
||||||
jwksVerifier, err = keyfunc.NewDefaultCtx(ctx, []string{
|
jwksVerifier, err = keyfunc.NewDefaultCtx(ctx, []string{
|
||||||
cfg.AuthJwtCertsURL,
|
cfg.AuthJwtCertsURL,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Init JWKS Failure: %v", err)
|
log.Errorf("Init JWKS Failure: %v", err)
|
||||||
}
|
jwksInitErr = err
|
||||||
|
|
||||||
defer cancel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
return jwksInitErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func readLocalPublicKey(cfg *config.Config) error {
|
func loadPublicKeyFromFile(keyPath string) error {
|
||||||
if pubKeyBytes != nil {
|
keyBytes, err := os.ReadFile(keyPath)
|
||||||
return nil // Already read.
|
|
||||||
}
|
|
||||||
|
|
||||||
pubKeyBytes, err := os.ReadFile(cfg.AuthJwtPubKeyPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("couldn't read public key from file %s", cfg.AuthJwtPubKeyPath)
|
return fmt.Errorf("couldn't read public key from file %s", keyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since the token is RSA (which we validated at the start of this function), the return type of this function actually has to be rsa.PublicKey!
|
parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(keyBytes)
|
||||||
pubKey, err = jwt.ParseRSAPublicKeyFromPEM(pubKeyBytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error parsing public key object (from %s)", cfg.AuthJwtPubKeyPath)
|
return fmt.Errorf("error parsing public key object (from %s)", keyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pubKeyBytes = keyBytes
|
||||||
|
pubKey = parsedKey
|
||||||
|
loadedKeyPath = keyPath
|
||||||
|
localKeyInitErr = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwtTokenWithRemoteKey(cfg *config.Config, jwtToken string) (*jwt.Token, error) {
|
func isKeyLoadedForPath(keyPath string) bool {
|
||||||
initJwks(cfg)
|
return pubKeyBytes != nil && loadedKeyPath == keyPath
|
||||||
|
}
|
||||||
|
|
||||||
return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud))
|
func readLocalPublicKeyWithLock(keyPath string) error {
|
||||||
|
localKeyMutex.RLock()
|
||||||
|
alreadyLoaded := isKeyLoadedForPath(keyPath)
|
||||||
|
localKeyMutex.RUnlock()
|
||||||
|
|
||||||
|
if alreadyLoaded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
localKeyMutex.Lock()
|
||||||
|
defer localKeyMutex.Unlock()
|
||||||
|
|
||||||
|
if isKeyLoadedForPath(keyPath) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
localKeyInitErr = loadPublicKeyFromFile(keyPath)
|
||||||
|
return localKeyInitErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLocalPublicKey(cfg *config.Config) error {
|
||||||
|
if cfg.AuthJwtPubKeyPath == "" {
|
||||||
|
return errors.New("no JWT public key path configured")
|
||||||
|
}
|
||||||
|
return readLocalPublicKeyWithLock(cfg.AuthJwtPubKeyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwtTokenWithLocalKey(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
func parseJwtTokenWithLocalKey(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
||||||
|
|
@ -97,33 +168,6 @@ func parseJwtTokenWithHMAC(cfg *config.Config, jwtString string) (*jwt.Token, er
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwtToken(cfg *config.Config, jwtString string) (*jwt.Token, error) {
|
|
||||||
if cfg.AuthJwtCertsURL != "" {
|
|
||||||
return parseJwtTokenWithRemoteKey(cfg, jwtString)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.AuthJwtPubKeyPath != "" {
|
|
||||||
return parseJwtTokenWithLocalKey(cfg, jwtString)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseJwtTokenWithHMAC(cfg, jwtString)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) {
|
|
||||||
token, err := parseJwtToken(cfg, jwtString)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("jwt parse failure: %v", err)
|
|
||||||
return nil, errors.New("jwt parse failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
|
||||||
return claims, nil
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("jwt token isn't valid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) string {
|
func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) string {
|
||||||
if val, ok := claims[key]; ok {
|
if val, ok := claims[key]; ok {
|
||||||
return fmt.Sprintf("%s", val)
|
return fmt.Sprintf("%s", val)
|
||||||
|
|
@ -132,33 +176,48 @@ func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwtCookie(cfg *config.Config, request *http.Request) (string, string) {
|
func CheckUserFromJwtCookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
|
||||||
cookie, err := request.Cookie(cfg.AuthJwtCookieName)
|
cookie, err := context.Request.Cookie(context.Config.AuthJwtCookieName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("jwt cookie check %v name: %v", err, cfg.AuthJwtCookieName)
|
log.Debugf("jwt cookie check %v name: %v", err, context.Config.AuthJwtCookieName)
|
||||||
return "", ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseJwt(cfg, cookie.Value)
|
return parseJwt(context.Config, cookie.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseJwt(cfg *config.Config, token string) (string, string) {
|
func CheckUserFromJwtHeader(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
|
||||||
|
header := context.Request.Header.Get(context.Config.AuthJwtHeader)
|
||||||
|
if header == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.TrimPrefix(header, "Bearer ")
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
|
||||||
|
return parseJwt(context.Config, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJwt(cfg *config.Config, token string) *authTypes.AuthenticatedUser {
|
||||||
claims, err := getClaimsFromJwtToken(cfg, token)
|
claims, err := getClaimsFromJwtToken(cfg, token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("jwt claim error: %+v", err)
|
log.Warnf("jwt claim error: %+v", err)
|
||||||
return "", ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.InsecureAllowDumpJwtClaims {
|
if cfg.InsecureAllowDumpJwtClaims {
|
||||||
log.Debugf("JWT Claims %+v", claims)
|
log.Debugf("JWT Claims %+v", claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
username := lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, "")
|
user := &authTypes.AuthenticatedUser{
|
||||||
usergroup := parseGroupClaim(cfg.AuthJwtClaimUserGroup, claims)
|
Username: lookupClaimValueOrDefault(claims, cfg.AuthJwtClaimUsername, ""),
|
||||||
|
UsergroupLine: parseGroupClaim(cfg.AuthJwtClaimUserGroup, claims),
|
||||||
|
Provider: "jwt",
|
||||||
|
}
|
||||||
|
|
||||||
return username, usergroup
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGroupClaim(groupClaim string, claims jwt.MapClaims) string {
|
func parseGroupClaim(groupClaim string, claims jwt.MapClaims) string {
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
package otjwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateRSAKeyPair(t *testing.T) (*rsa.PrivateKey, []byte) {
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate RSA key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey := &privateKey.PublicKey
|
||||||
|
pkixPubKey, err := x509.MarshalPKIXPublicKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal public key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubPem := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: pkixPubKey,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return privateKey, pubPem
|
||||||
|
}
|
||||||
|
|
||||||
|
func createKeys(t *testing.T) (*rsa.PrivateKey, string) {
|
||||||
|
tmpFile, err := os.CreateTemp(os.TempDir(), "olivetin-jwt-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp file: %v", err)
|
||||||
|
}
|
||||||
|
defer tmpFile.Close()
|
||||||
|
|
||||||
|
t.Logf("Created File: %s", tmpFile.Name())
|
||||||
|
|
||||||
|
privateKey, pubPem := generateRSAKeyPair(t)
|
||||||
|
|
||||||
|
if err := os.WriteFile(tmpFile.Name(), pubPem, 0644); err != nil {
|
||||||
|
t.Fatalf("error when dumping pubKey: %s \n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return privateKey, tmpFile.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMux() *http.ServeMux {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func createJWTTokenWithExpiration(t *testing.T, privateKey *rsa.PrivateKey, expire int64) string {
|
||||||
|
token := jwt.New(jwt.SigningMethodRS256)
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
claims["nbf"] = time.Now().Unix() - 1000
|
||||||
|
claims["exp"] = time.Now().Unix() + expire
|
||||||
|
claims["sub"] = "test"
|
||||||
|
claims["olivetinGroup"] = "test"
|
||||||
|
|
||||||
|
tokenStr, err := token.SignedString(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to sign JWT token: %v", err)
|
||||||
|
}
|
||||||
|
return tokenStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupJWTTestHandler(t *testing.T, cfg *config.Config) http.Handler {
|
||||||
|
mux := newMux()
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
context := &authpublic.AuthCheckingContext{
|
||||||
|
Request: r,
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
user := CheckUserFromJwtHeader(context)
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
w.WriteHeader(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "test", user.Username)
|
||||||
|
assert.Equal(t, "test", user.UsergroupLine)
|
||||||
|
})
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyJWTResponse(t *testing.T, res *http.Response, expectCode int) {
|
||||||
|
defer res.Body.Close()
|
||||||
|
assert.Equal(t, expectCode, res.StatusCode)
|
||||||
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
t.Logf("Response body: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testJwkValidation(t *testing.T, expire int64, expectCode int) {
|
||||||
|
privateKey, publicKeyPath := createKeys(t)
|
||||||
|
defer os.Remove(publicKeyPath)
|
||||||
|
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||||
|
cfg.AuthJwtClaimUsername = "sub"
|
||||||
|
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
|
||||||
|
cfg.AuthJwtHeader = "Authorization"
|
||||||
|
|
||||||
|
tokenStr := createJWTTokenWithExpiration(t, privateKey, expire)
|
||||||
|
handler := setupJWTTestHandler(t, cfg)
|
||||||
|
|
||||||
|
srv := httptest.NewServer(handler)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
res := makeJWTRequest(t, srv, tokenStr)
|
||||||
|
verifyJWTResponse(t, res, expectCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJWTSignatureVerificationSucceeds(t *testing.T) {
|
||||||
|
testJwkValidation(t, 1000, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJWTSignatureVerificationFails(t *testing.T) {
|
||||||
|
testJwkValidation(t, -500, 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createJWTTokenWithGroups(t *testing.T, privateKey *rsa.PrivateKey, groups interface{}) string {
|
||||||
|
token := jwt.New(jwt.SigningMethodRS256)
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
claims["nbf"] = time.Now().Unix() - 1000
|
||||||
|
claims["exp"] = time.Now().Unix() + 2000
|
||||||
|
claims["sub"] = "test"
|
||||||
|
claims["olivetinGroup"] = groups
|
||||||
|
|
||||||
|
tokenStr, err := token.SignedString(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to sign JWT token: %v", err)
|
||||||
|
}
|
||||||
|
return tokenStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeJWTRequest(t *testing.T, srv *httptest.Server, tokenStr string) *http.Response {
|
||||||
|
req, err := http.NewRequest("GET", srv.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+tokenStr)
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Client err: %+v", err)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJWTHeader(t *testing.T) {
|
||||||
|
privateKey, publicKeyPath := createKeys(t)
|
||||||
|
defer os.Remove(publicKeyPath)
|
||||||
|
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.AuthJwtPubKeyPath = publicKeyPath
|
||||||
|
cfg.AuthJwtClaimUsername = "sub"
|
||||||
|
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
|
||||||
|
cfg.AuthJwtHeader = "Authorization"
|
||||||
|
|
||||||
|
tokenStr := createJWTTokenWithGroups(t, privateKey, []string{"test", "test2"})
|
||||||
|
|
||||||
|
mux := newMux()
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
context := &authpublic.AuthCheckingContext{
|
||||||
|
Request: r,
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
user := CheckUserFromJwtHeader(context)
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
w.WriteHeader(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "test", user.Username)
|
||||||
|
assert.Equal(t, "test test2", user.UsergroupLine)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := httptest.NewServer(mux)
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
res := makeJWTRequest(t, srv, tokenStr)
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, 200, res.StatusCode)
|
||||||
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
t.Logf("Response body: %s", string(body))
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package httpservers
|
package otoauth2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -8,13 +8,15 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OAuth2Handler struct {
|
type OAuth2Handler struct {
|
||||||
|
|
@ -110,7 +112,7 @@ func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Re
|
||||||
cookie := &http.Cookie{
|
cookie := &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: value,
|
Value: value,
|
||||||
MaxAge: 31556952, // 1 year
|
MaxAge: 900, // 15 minutes
|
||||||
Secure: r.TLS != nil,
|
Secure: r.TLS != nil,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
|
|
@ -119,7 +121,7 @@ func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Re
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *OAuth2Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
func (h *OAuth2Handler) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
state, err := randString(16)
|
state, err := randString(16)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -149,30 +151,31 @@ func (h *OAuth2Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request)
|
||||||
http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
|
http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *OAuth2Handler) validateStateMatch(queryState, cookieState string) bool {
|
||||||
|
return queryState == cookieState
|
||||||
|
}
|
||||||
|
|
||||||
func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
|
func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
|
||||||
cookie, err := r.Cookie("olivetin-sid-oauth")
|
cookie, err := r.Cookie("olivetin-sid-oauth")
|
||||||
state := cookie.Value
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to get state cookie: %v", err)
|
log.Errorf("Failed to get state cookie: %v", err)
|
||||||
|
|
||||||
http.Error(w, "State not found", http.StatusBadRequest)
|
http.Error(w, "State not found", http.StatusBadRequest)
|
||||||
return nil, state, false
|
return nil, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.URL.Query().Get("state") != state {
|
state := cookie.Value
|
||||||
log.Errorf("State mismatch: %v != %v", r.URL.Query().Get("state"), state)
|
|
||||||
|
|
||||||
|
if !h.validateStateMatch(r.URL.Query().Get("state"), state) {
|
||||||
|
log.Errorf("State mismatch: %v != %v", r.URL.Query().Get("state"), state)
|
||||||
http.Error(w, "State mismatch", http.StatusBadRequest)
|
http.Error(w, "State mismatch", http.StatusBadRequest)
|
||||||
return nil, state, false
|
return nil, state, false
|
||||||
}
|
}
|
||||||
|
|
||||||
registeredState, ok := h.registeredStates[state]
|
registeredState, ok := h.registeredStates[state]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("State not found in server: %v", state)
|
log.Errorf("State not found in server: %v", state)
|
||||||
|
|
||||||
http.Error(w, "State not found in server", http.StatusBadRequest)
|
http.Error(w, "State not found in server", http.StatusBadRequest)
|
||||||
|
return nil, state, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return registeredState, state, true
|
return registeredState, state, true
|
||||||
|
|
@ -210,13 +213,13 @@ func getOAuthCertBundle(providerConfig *config.OAuth2Provider) *x509.CertPool {
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
|
|
||||||
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
|
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
|
||||||
log.Errorf("OAuth2 Cert Bundle - failed to append certificates: %v", err)
|
log.Errorf("OAuth2 Cert Bundle - failed to append certificates from PEM")
|
||||||
}
|
}
|
||||||
|
|
||||||
return caCertPool
|
return caCertPool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *OAuth2Handler) handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
func (h *OAuth2Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Infof("OAuth2 Callback received")
|
log.Infof("OAuth2 Callback received")
|
||||||
|
|
||||||
registeredState, state, ok := h.checkOAuthCallbackCookie(w, r)
|
registeredState, state, ok := h.checkOAuthCallbackCookie(w, r)
|
||||||
|
|
@ -265,17 +268,7 @@ func (h *OAuth2Handler) handleOAuthCallback(w http.ResponseWriter, r *http.Reque
|
||||||
h.registeredStates[state].Username = userinfo.Username
|
h.registeredStates[state].Username = userinfo.Username
|
||||||
h.registeredStates[state].Usergroup = userinfo.Usergroup
|
h.registeredStates[state].Usergroup = userinfo.Usergroup
|
||||||
|
|
||||||
for k, v := range h.registeredStates {
|
|
||||||
log.Debugf("states: %+v %+v", k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"state": state,
|
|
||||||
"username": h.registeredStates[state].Username,
|
|
||||||
}).Info("OAuth2 login successful")
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
w.Write([]byte("OAuth2 login successful."))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
|
|
@ -351,16 +344,18 @@ func getDataField(data map[string]any, field string) string {
|
||||||
return stringVal
|
return stringVal
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *OAuth2Handler) parseOAuth2Cookie(r *http.Request) (string, string, string) {
|
func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
|
||||||
cookie, err := r.Cookie("olivetin-sid-oauth")
|
cookie, err := context.Request.Cookie("olivetin-sid-oauth")
|
||||||
|
|
||||||
|
user := &authTypes.AuthenticatedUser{}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Failed to read OAuth2 cookie: %v", err)
|
log.Warnf("Failed to read OAuth2 cookie: %v", err)
|
||||||
return "", "", ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if cookie.Value == "" {
|
if cookie.Value == "" {
|
||||||
return "", "", ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
serverState, found := h.registeredStates[cookie.Value]
|
serverState, found := h.registeredStates[cookie.Value]
|
||||||
|
|
@ -371,10 +366,13 @@ func (h *OAuth2Handler) parseOAuth2Cookie(r *http.Request) (string, string, stri
|
||||||
"provider": "oauth2",
|
"provider": "oauth2",
|
||||||
}).Warnf("Stale session")
|
}).Warnf("Stale session")
|
||||||
|
|
||||||
return "", "", cookie.Value
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Found OAuth2 state: %+v", serverState)
|
user.Username = serverState.Username
|
||||||
|
user.UsergroupLine = serverState.Usergroup
|
||||||
|
user.Provider = "oauth2"
|
||||||
|
user.SID = cookie.Value
|
||||||
|
|
||||||
return serverState.Username, serverState.Usergroup, cookie.Value
|
return user
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package httpservers
|
package otoauth2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
|
@ -13,11 +13,14 @@ var oauth2ProviderDatabase = map[string]config.OAuth2Provider{
|
||||||
WhoamiUrl: "https://api.github.com/user",
|
WhoamiUrl: "https://api.github.com/user",
|
||||||
TokenUrl: endpoints.GitHub.TokenURL,
|
TokenUrl: endpoints.GitHub.TokenURL,
|
||||||
AuthUrl: endpoints.GitHub.AuthURL,
|
AuthUrl: endpoints.GitHub.AuthURL,
|
||||||
Scopes: []string{"profile", "email"},
|
Scopes: []string{"read:user", "user:email"},
|
||||||
UsernameField: "login",
|
UsernameField: "login",
|
||||||
},
|
},
|
||||||
"google": {
|
"google": {
|
||||||
|
Title: "Google",
|
||||||
|
Name: "google",
|
||||||
Icon: "google",
|
Icon: "google",
|
||||||
|
UsernameField: "email",
|
||||||
WhoamiUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
|
WhoamiUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
|
||||||
TokenUrl: endpoints.Google.TokenURL,
|
TokenUrl: endpoints.Google.TokenURL,
|
||||||
AuthUrl: endpoints.Google.AuthURL,
|
AuthUrl: endpoints.Google.AuthURL,
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UserGuest(cfg *config.Config) *authpublic.AuthenticatedUser {
|
||||||
|
ret := &authpublic.AuthenticatedUser{}
|
||||||
|
ret.Username = "guest"
|
||||||
|
ret.UsergroupLine = "guest"
|
||||||
|
ret.Provider = "system"
|
||||||
|
|
||||||
|
ret.BuildUserAcls(cfg)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserFromSystem(cfg *config.Config, username string) *authpublic.AuthenticatedUser {
|
||||||
|
ret := &authpublic.AuthenticatedUser{
|
||||||
|
Username: username,
|
||||||
|
UsergroupLine: "system",
|
||||||
|
Provider: "system",
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.BuildUserAcls(cfg)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
types "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
|
)
|
||||||
|
|
||||||
|
//gocyclo:ignore
|
||||||
|
func checkUserFromHeaders(context *types.AuthCheckingContext) *types.AuthenticatedUser {
|
||||||
|
u := &types.AuthenticatedUser{}
|
||||||
|
|
||||||
|
if context.Config.AuthHttpHeaderUsername != "" {
|
||||||
|
u.Username = getHeaderKeyOrEmpty(context.Request.Header, context.Config.AuthHttpHeaderUsername)
|
||||||
|
}
|
||||||
|
|
||||||
|
if context.Config.AuthHttpHeaderUserGroup != "" {
|
||||||
|
u.UsergroupLine = getHeaderKeyOrEmpty(context.Request.Header, context.Config.AuthHttpHeaderUserGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prov := getHeaderKeyOrEmpty(context.Request.Header, "provider"); prov != "" {
|
||||||
|
u.Provider = prov
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHeaderKeyOrEmpty(headers http.Header, key string) string {
|
||||||
|
values := headers.Values(key)
|
||||||
|
if len(values) > 0 {
|
||||||
|
return values[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
@ -333,12 +333,13 @@ func mangleCheckboxValues(req *ExecutionRequest, arg *config.ActionArgument) {
|
||||||
|
|
||||||
log.Infof("Checking checkbox values for argument %s in action %s", arg.Name, req.Binding.Action.Title)
|
log.Infof("Checking checkbox values for argument %s in action %s", arg.Name, req.Binding.Action.Title)
|
||||||
|
|
||||||
for i, _ := range arg.Choices {
|
for i, v := range arg.Choices {
|
||||||
choice := &arg.Choices[i]
|
choice := &arg.Choices[i]
|
||||||
|
|
||||||
if req.Arguments[arg.Name] == choice.Title {
|
if req.Arguments[arg.Name] == choice.Title {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"arg": arg.Name,
|
"arg": arg.Name,
|
||||||
|
"choice": v,
|
||||||
"oldValue": req.Arguments[arg.Name],
|
"oldValue": req.Arguments[arg.Name],
|
||||||
"newValue": choice.Value,
|
"newValue": choice.Value,
|
||||||
"actionTitle": req.Binding.Action.Title,
|
"actionTitle": req.Binding.Action.Title,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/auth"
|
||||||
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/entities"
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
@ -69,7 +71,7 @@ type ExecutionRequest struct {
|
||||||
TrackingID string
|
TrackingID string
|
||||||
Tags []string
|
Tags []string
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
AuthenticatedUser *acl.AuthenticatedUser
|
AuthenticatedUser *authpublic.AuthenticatedUser
|
||||||
TriggerDepth int
|
TriggerDepth int
|
||||||
|
|
||||||
logEntry *InternalLogEntry
|
logEntry *InternalLogEntry
|
||||||
|
|
@ -230,11 +232,11 @@ func isValidLogEntryForACL(entry *InternalLogEntry) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// isLogEntryAllowedByACL checks if a log entry is allowed to be viewed by the user.
|
// isLogEntryAllowedByACL checks if a log entry is allowed to be viewed by the user.
|
||||||
func isLogEntryAllowedByACL(cfg *config.Config, user *acl.AuthenticatedUser, entry *InternalLogEntry) bool {
|
func isLogEntryAllowedByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, entry *InternalLogEntry) bool {
|
||||||
return acl.IsAllowedLogs(cfg, user, entry.Binding.Action)
|
return acl.IsAllowedLogs(cfg, user, entry.Binding.Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) filterLogsByACL(cfg *config.Config, user *acl.AuthenticatedUser) []*InternalLogEntry {
|
func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.AuthenticatedUser) []*InternalLogEntry {
|
||||||
e.logmutex.RLock()
|
e.logmutex.RLock()
|
||||||
defer e.logmutex.RUnlock()
|
defer e.logmutex.RUnlock()
|
||||||
|
|
||||||
|
|
@ -280,7 +282,7 @@ func paginateFilteredLogs(filtered []*InternalLogEntry, startOffset int64, pageC
|
||||||
|
|
||||||
// GetLogTrackingIdsACL returns logs filtered by ACL visibility for the user and
|
// GetLogTrackingIdsACL returns logs filtered by ACL visibility for the user and
|
||||||
// paginated correctly based on the filtered set.
|
// paginated correctly based on the filtered set.
|
||||||
func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *acl.AuthenticatedUser, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
|
func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *authpublic.AuthenticatedUser, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
|
||||||
filtered := e.filterLogsByACL(cfg, user)
|
filtered := e.filterLogsByACL(cfg, user)
|
||||||
return paginateFilteredLogs(filtered, startOffset, pageCount)
|
return paginateFilteredLogs(filtered, startOffset, pageCount)
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +325,7 @@ func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
|
||||||
// ExecRequest processes an ExecutionRequest
|
// ExecRequest processes an ExecutionRequest
|
||||||
func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
|
func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
|
||||||
if req.AuthenticatedUser == nil {
|
if req.AuthenticatedUser == nil {
|
||||||
req.AuthenticatedUser = acl.UserGuest(req.Cfg)
|
req.AuthenticatedUser = auth.UserGuest(req.Cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.executor = e
|
req.executor = e
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
"github.com/OliveTin/OliveTin/internal/auth"
|
||||||
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -35,7 +36,7 @@ func TestCreateExecutorAndExec(t *testing.T) {
|
||||||
e, cfg := testingExecutor()
|
e, cfg := testingExecutor()
|
||||||
|
|
||||||
req := ExecutionRequest{
|
req := ExecutionRequest{
|
||||||
AuthenticatedUser: &acl.AuthenticatedUser{Username: "Mr Tickle"},
|
AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "Mr Tickle"},
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Arguments: map[string]string{
|
Arguments: map[string]string{
|
||||||
"person": "yourself",
|
"person": "yourself",
|
||||||
|
|
@ -273,7 +274,7 @@ func TestMangleInvalidArgumentValues(t *testing.T) {
|
||||||
|
|
||||||
req := ExecutionRequest{
|
req := ExecutionRequest{
|
||||||
// Action: a1,
|
// Action: a1,
|
||||||
AuthenticatedUser: acl.UserFromSystem(cfg, "testuser"),
|
AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Arguments: map[string]string{
|
Arguments: map[string]string{
|
||||||
"date": "1990-01-10T12:00", // Invalid format, should be without seconds
|
"date": "1990-01-10T12:00", // Invalid format, should be without seconds
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import (
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/OliveTin/OliveTin/internal/api"
|
"github.com/OliveTin/OliveTin/internal/api"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/auth"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/auth/otoauth2"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
@ -32,13 +34,13 @@ func logDebugRequest(cfg *config.Config, source string, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartSingleHTTPFrontend will create a reverse proxy that proxies the API
|
func StartFrontendMux(cfg *config.Config, ex *executor.Executor) {
|
||||||
// and webui internally.
|
|
||||||
func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"address": cfg.ListenAddressSingleHTTPFrontend,
|
"address": cfg.ListenAddressSingleHTTPFrontend,
|
||||||
}).Info("Starting single HTTP frontend")
|
}).Info("Starting single HTTP frontend")
|
||||||
|
|
||||||
|
go StartPrometheus(cfg)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
apiPath, apiHandler := api.GetNewHandler(ex)
|
apiPath, apiHandler := api.GetNewHandler(ex)
|
||||||
|
|
@ -62,10 +64,11 @@ func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
|
||||||
apiHandler.ServeHTTP(w, r)
|
apiHandler.ServeHTTP(w, r)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
oauth2handler := NewOAuth2Handler(cfg)
|
oauth2handler := otoauth2.NewOAuth2Handler(cfg)
|
||||||
|
auth.AddAuthChainFunction(oauth2handler.CheckUserFromOAuth2Cookie)
|
||||||
|
|
||||||
mux.HandleFunc("/oauth/login", oauth2handler.handleOAuthLogin)
|
mux.HandleFunc("/oauth/login", oauth2handler.HandleOAuthLogin)
|
||||||
mux.HandleFunc("/oauth/callback", oauth2handler.handleOAuthCallback)
|
mux.HandleFunc("/oauth/callback", oauth2handler.HandleOAuthCallback)
|
||||||
|
|
||||||
mux.HandleFunc("/readyz", handleReadyz)
|
mux.HandleFunc("/readyz", handleReadyz)
|
||||||
|
|
||||||
|
|
@ -97,5 +100,11 @@ func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
|
||||||
func handleReadyz(w http.ResponseWriter, r *http.Request) {
|
func handleReadyz(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("OK. Single HTTP Frontend is ready.\n"))
|
_, err := w.Write([]byte("OK. Single HTTP Frontend is ready.\n"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Warnf("Failed to write readyz response")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package httpservers
|
|
||||||
|
|
||||||
import (
|
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StartServers will start 3 HTTP servers. The WebUI, the Rest API, and a proxy
|
|
||||||
// for both of them.
|
|
||||||
func StartServers(cfg *config.Config, ex *executor.Executor) {
|
|
||||||
if cfg.Prometheus.Enabled {
|
|
||||||
go StartPrometheus(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
StartSingleHTTPFrontend(cfg, ex)
|
|
||||||
}
|
|
||||||
|
|
@ -7,13 +7,25 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartPrometheus(cfg *config.Config) {
|
func StartPrometheus(cfg *config.Config) {
|
||||||
|
if !cfg.Prometheus.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !cfg.Prometheus.DefaultGoMetrics {
|
if !cfg.Prometheus.DefaultGoMetrics {
|
||||||
prometheus.Unregister(collectors.NewGoCollector())
|
prometheus.Unregister(collectors.NewGoCollector())
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Handle("/", promhttp.Handler())
|
http.Handle("/", promhttp.Handler())
|
||||||
http.ListenAndServe(cfg.ListenAddressPrometheus, nil)
|
err := http.ListenAndServe(cfg.ListenAddressPrometheus, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"address": cfg.ListenAddressPrometheus,
|
||||||
|
"error": err,
|
||||||
|
}).Warnf("Failed to start Prometheus server")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
package httpservers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
// config "github.com/OliveTin/OliveTin/internal/config"
|
|
||||||
// "github.com/golang-jwt/jwt/v4"
|
|
||||||
// "github.com/stretchr/testify/assert"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
// "time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createKeys(t *testing.T) (*rsa.PrivateKey, string) {
|
|
||||||
tmpFile, _ := os.CreateTemp(os.TempDir(), "olivetin-jwt-")
|
|
||||||
|
|
||||||
fmt.Println("Created File: " + tmpFile.Name())
|
|
||||||
|
|
||||||
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
pubKey := &privateKey.PublicKey
|
|
||||||
// https://stackoverflow.com/questions/13555085/save-and-load-crypto-rsa-privatekey-to-and-from-the-disk
|
|
||||||
pkixPubKey, _ := x509.MarshalPKIXPublicKey(pubKey)
|
|
||||||
pubPem := pem.EncodeToMemory(
|
|
||||||
&pem.Block{
|
|
||||||
Type: "RSA PUBLIC KEY",
|
|
||||||
Bytes: pkixPubKey,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := os.WriteFile(tmpFile.Name(), pubPem, 0755); err != nil {
|
|
||||||
t.Fatalf("error when dumping pubKey: %s \n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return privateKey, tmpFile.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMux() *http.ServeMux {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
return mux
|
|
||||||
}
|
|
||||||
|
|
||||||
func testJwkValidation(t *testing.T, expire int64, expectCode int) {
|
|
||||||
/*
|
|
||||||
privateKey, publicKeyPath := createKeys(t)
|
|
||||||
|
|
||||||
defer os.Remove(publicKeyPath)
|
|
||||||
|
|
||||||
cfg := config.DefaultConfig()
|
|
||||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
|
||||||
cfg.AuthJwtClaimUsername = "sub"
|
|
||||||
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
|
|
||||||
cfg.AuthJwtCookieName = "authorization_token"
|
|
||||||
|
|
||||||
token := jwt.New(jwt.SigningMethodRS256)
|
|
||||||
|
|
||||||
claims := token.Claims.(jwt.MapClaims)
|
|
||||||
claims["nbf"] = time.Now().Unix() - 1000
|
|
||||||
claims["exp"] = time.Now().Unix() + expire
|
|
||||||
claims["sub"] = "test"
|
|
||||||
claims["olivetinGroup"] = "test"
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
tokenStr, _ := token.SignedString(privateKey)
|
|
||||||
|
|
||||||
mux := newMux()
|
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
|
||||||
username, usergroup := parseJwtCookie(cfg, r)
|
|
||||||
|
|
||||||
if username == "" {
|
|
||||||
w.WriteHeader(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
|
|
||||||
})
|
|
||||||
|
|
||||||
srv := setupTestingServer(mux, t)
|
|
||||||
|
|
||||||
req, client := newReq("")
|
|
||||||
req.AddCookie(&http.Cookie{
|
|
||||||
Name: "authorization_token",
|
|
||||||
Value: tokenStr,
|
|
||||||
MaxAge: 300,
|
|
||||||
})
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Client err: %+v", err)
|
|
||||||
} else {
|
|
||||||
defer res.Body.Close()
|
|
||||||
assert.Equal(t, expectCode, res.StatusCode)
|
|
||||||
body, _ := io.ReadAll(res.Body)
|
|
||||||
fmt.Println(string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = srv.Shutdown(context.TODO())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Server shutdown error: %+v", err)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWTSignatureVerificationSucceeds(t *testing.T) {
|
|
||||||
testJwkValidation(t, 1000, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWTSignatureVerificationFails(t *testing.T) {
|
|
||||||
testJwkValidation(t, -500, 403)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWTHeader(t *testing.T) {
|
|
||||||
/*
|
|
||||||
privateKey, publicKeyPath := createKeys(t)
|
|
||||||
|
|
||||||
defer os.Remove(publicKeyPath)
|
|
||||||
|
|
||||||
cfg := config.DefaultConfig()
|
|
||||||
cfg.AuthJwtPubKeyPath = publicKeyPath
|
|
||||||
cfg.AuthJwtClaimUsername = "sub"
|
|
||||||
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
|
|
||||||
cfg.AuthJwtHeader = "Authorization"
|
|
||||||
|
|
||||||
token := jwt.New(jwt.SigningMethodRS256)
|
|
||||||
|
|
||||||
claims := token.Claims.(jwt.MapClaims)
|
|
||||||
claims["nbf"] = time.Now().Unix() - 1000
|
|
||||||
claims["exp"] = time.Now().Unix() + 2000
|
|
||||||
claims["sub"] = "test"
|
|
||||||
claims["olivetinGroup"] = []string{"test", "test2"}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
tokenStr, _ := token.SignedString(privateKey)
|
|
||||||
|
|
||||||
mux := newMux()
|
|
||||||
mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
|
||||||
username, usergroup := parseJwtHeader(cfg, r)
|
|
||||||
|
|
||||||
if username == "" {
|
|
||||||
w.WriteHeader(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "test", username)
|
|
||||||
assert.Equal(t, "test test2", usergroup)
|
|
||||||
|
|
||||||
w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
|
|
||||||
})
|
|
||||||
|
|
||||||
srv := setupTestingServer(mux, t)
|
|
||||||
|
|
||||||
req, client := newReq("")
|
|
||||||
req.Header.Set("Authorization", "Bearer "+tokenStr)
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Client err: %+v", err)
|
|
||||||
} else {
|
|
||||||
defer res.Body.Close()
|
|
||||||
assert.Equal(t, 200, res.StatusCode)
|
|
||||||
body, _ := io.ReadAll(res.Body)
|
|
||||||
fmt.Println(string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.Shutdown(context.TODO())
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package httpservers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/OliveTin/OliveTin/internal/auth"
|
|
||||||
"github.com/OliveTin/OliveTin/internal/config"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func parseLocalUserCookie(cfg *config.Config, req *http.Request) (string, string, string) {
|
|
||||||
cookie, err := req.Cookie("olivetin-sid-local")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
cookieValue := cookie.Value
|
|
||||||
|
|
||||||
session := auth.GetUserSession("local", cookieValue)
|
|
||||||
if session == nil {
|
|
||||||
return "", "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
user := cfg.FindUserByUsername(session.Username)
|
|
||||||
if user == nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"username": session.Username,
|
|
||||||
}).Warnf("User not found in config")
|
|
||||||
return "", "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.Username, user.Usergroup, cookie.Value
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
package httpservers
|
|
||||||
|
|
||||||
import ()
|
|
||||||
|
|
@ -93,22 +93,42 @@ func (s *webUIServer) setupCustomWebuiDir() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldReloadThemeCss() bool {
|
||||||
|
return !customThemeCssRead
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadThemeCssFromFile(filename string) []byte {
|
||||||
|
_, err := os.Stat(filename)
|
||||||
|
if err == nil {
|
||||||
|
css, err := os.ReadFile(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Tracef("Theme CSS file not read: %s", filename)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return css
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Theme CSS file not found: %s", filename)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *webUIServer) generateThemeCss(w http.ResponseWriter, r *http.Request) {
|
func (s *webUIServer) generateThemeCss(w http.ResponseWriter, r *http.Request) {
|
||||||
themeCssFilename := path.Join(s.findCustomWebuiDir(), "themes", s.cfg.ThemeName, "theme.css")
|
themeCssFilename := path.Join(s.findCustomWebuiDir(), "themes", s.cfg.ThemeName, "theme.css")
|
||||||
|
|
||||||
if !customThemeCssRead || s.cfg.ThemeCacheDisabled {
|
if shouldReloadThemeCss() || s.cfg.ThemeCacheDisabled {
|
||||||
customThemeCssRead = true
|
customThemeCssRead = true
|
||||||
|
customThemeCss = loadThemeCssFromFile(themeCssFilename)
|
||||||
if _, err := os.Stat(themeCssFilename); err == nil {
|
|
||||||
customThemeCss, _ = os.ReadFile(themeCssFilename)
|
|
||||||
} else {
|
|
||||||
log.Debugf("Theme CSS not read: %v", err)
|
|
||||||
customThemeCss = []byte("/* not found */")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/css")
|
w.Header().Add("Content-Type", "text/css")
|
||||||
w.Write(customThemeCss)
|
_, err := w.Write(customThemeCss)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Warnf("Failed to write theme CSS")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *webUIServer) handleCustomWebui() http.Handler {
|
func (s *webUIServer) handleCustomWebui() http.Handler {
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,15 @@ package oncalendarfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/OliveTin/OliveTin/internal/acl"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/auth"
|
||||||
"github.com/OliveTin/OliveTin/internal/config"
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
"github.com/OliveTin/OliveTin/internal/filehelper"
|
"github.com/OliveTin/OliveTin/internal/filehelper"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Schedule(cfg *config.Config, ex *executor.Executor) {
|
func Schedule(cfg *config.Config, ex *executor.Executor) {
|
||||||
|
|
@ -105,7 +106,7 @@ func exec(instant time.Time, action *config.Action, cfg *config.Config, ex *exec
|
||||||
Binding: ex.FindBindingWithNoEntity(action),
|
Binding: ex.FindBindingWithNoEntity(action),
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
AuthenticatedUser: acl.UserFromSystem(cfg, "calendar"),
|
AuthenticatedUser: auth.UserFromSystem(cfg, "calendar"),
|
||||||
}
|
}
|
||||||
|
|
||||||
ex.ExecRequest(req)
|
ex.ExecRequest(req)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package oncron
|
package oncron
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/OliveTin/OliveTin/internal/acl"
|
"github.com/OliveTin/OliveTin/internal/auth"
|
||||||
"github.com/OliveTin/OliveTin/internal/config"
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
|
|
@ -37,7 +37,7 @@ func scheduleAction(cfg *config.Config, scheduler *cron.Cron, cronline string, e
|
||||||
Binding: ex.FindBindingWithNoEntity(action),
|
Binding: ex.FindBindingWithNoEntity(action),
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
AuthenticatedUser: acl.UserFromSystem(cfg, "cron"),
|
AuthenticatedUser: auth.UserFromSystem(cfg, "cron"),
|
||||||
}
|
}
|
||||||
|
|
||||||
ex.ExecRequest(req)
|
ex.ExecRequest(req)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/OliveTin/OliveTin/internal/acl"
|
"github.com/OliveTin/OliveTin/internal/auth"
|
||||||
"github.com/OliveTin/OliveTin/internal/config"
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
"github.com/OliveTin/OliveTin/internal/filehelper"
|
"github.com/OliveTin/OliveTin/internal/filehelper"
|
||||||
|
|
@ -54,7 +54,7 @@ func scheduleExec(action *config.Action, cfg *config.Config, ex *executor.Execut
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
Arguments: args,
|
Arguments: args,
|
||||||
AuthenticatedUser: acl.UserFromSystem(cfg, "fileindir"),
|
AuthenticatedUser: auth.UserFromSystem(cfg, "fileindir"),
|
||||||
}
|
}
|
||||||
|
|
||||||
ex.ExecRequest(req)
|
ex.ExecRequest(req)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
package onstartup
|
package onstartup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/OliveTin/OliveTin/internal/acl"
|
"github.com/OliveTin/OliveTin/internal/auth"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Execute(cfg *config.Config, ex *executor.Executor) {
|
func Execute(cfg *config.Config, ex *executor.Executor) {
|
||||||
user := acl.UserFromSystem(cfg, "startup")
|
user := auth.UserFromSystem(cfg, "startup")
|
||||||
|
|
||||||
for _, action := range cfg.Actions {
|
for _, action := range cfg.Actions {
|
||||||
if action.ExecOnStartup {
|
if action.ExecOnStartup {
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,13 @@ func getConfigPath(directory string) string {
|
||||||
|
|
||||||
func initConfig(configDir string) {
|
func initConfig(configDir string) {
|
||||||
k := koanf.New(".")
|
k := koanf.New(".")
|
||||||
k.Load(env.Provider(".", ".", nil), nil)
|
err := k.Load(env.Provider(".", ".", nil), nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Fatalf("Error loading environment variables")
|
||||||
|
}
|
||||||
|
|
||||||
directories := []string{
|
directories := []string{
|
||||||
configDir,
|
configDir,
|
||||||
|
|
@ -180,13 +186,26 @@ func initConfig(configDir string) {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Watch(func(evt interface{}, err error) {
|
err := f.Watch(func(evt interface{}, err error) {
|
||||||
log.Infof("config file changed: %v", evt)
|
log.Infof("config file changed: %v", evt)
|
||||||
|
|
||||||
k.Load(f, yaml.Parser())
|
errLoad := k.Load(f, yaml.Parser())
|
||||||
|
|
||||||
|
if errLoad != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": errLoad,
|
||||||
|
}).Fatalf("Error loading config file")
|
||||||
|
}
|
||||||
|
|
||||||
config.AppendSource(cfg, k, configPath)
|
config.AppendSource(cfg, k, configPath)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"error": err,
|
||||||
|
}).Fatalf("Error watching config file")
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,5 +270,5 @@ func main() {
|
||||||
// Load persistent sessions from disk
|
// Load persistent sessions from disk
|
||||||
auth.LoadUserSessions(cfg)
|
auth.LoadUserSessions(cfg)
|
||||||
|
|
||||||
httpservers.StartServers(cfg, executor)
|
httpservers.StartFrontendMux(cfg, executor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue