Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This file contains all the latest changes and updates to Postal.

## [Unreleased]

### Features

* **oidc:** add optional auto-creation of users during OIDC login when enabled via `OIDC_AUTO_CREATE_USERS`
* **oidc:** optionally create a default organization for auto-provisioned users via `OIDC_AUTO_CREATE_ORGANIZATION`

## [3.3.4](https://github.com/postalserver/postal/compare/3.3.3...3.3.4) (2024-06-20)


Expand Down
69 changes: 65 additions & 4 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,83 @@ def find_from_oidc(auth, logger: nil)
end
end

# now, if we still don't have a user, we're not going to create one so we'll just
# return nil (we might auto create users in the future but not right now)
return if user.nil?
# If we still don't have a user and auto creation is enabled, create one from the OIDC details.
if user.nil?
if config.auto_create_users?
user = auto_create_user_from_oidc(uid, config, oidc_name, oidc_email_address, logger)
else
logger&.debug "OIDC auto user creation disabled; not creating user for #{oidc_email_address || uid}"
end
return if user.nil?
end

# otherwise, let's update our user as appropriate
user.oidc_uid = uid
user.oidc_issuer = config.issuer
user.email_address = oidc_email_address if oidc_email_address.present?
user.first_name, user.last_name = oidc_name.split(/\s+/, 2) if oidc_name.present?
if oidc_name.present?
user.first_name, user.last_name = derive_user_names_from_oidc(oidc_name, user.email_address)
end
user.password = nil
user.save!

# return the user
user
end

private

def auto_create_user_from_oidc(uid, config, oidc_name, oidc_email_address, logger)
unless oidc_email_address.present?
logger&.warn "OIDC auto user creation failed for UID #{uid}: no e-mail address provided"
return nil
end

first_name, last_name = derive_user_names_from_oidc(oidc_name, oidc_email_address)
user = new(
email_address: oidc_email_address,
first_name: first_name,
last_name: last_name
)
user.oidc_uid = uid
user.oidc_issuer = config.issuer
user.password = nil
user.save!
logger&.info "OIDC auto user creation succeeded for #{oidc_email_address} (user ID: #{user.id}) with Firstname: #{first_name}, Lastname: #{last_name}"
auto_create_organization_for(user, config, logger) if config.auto_create_organization?
user
rescue ActiveRecord::RecordInvalid => e
logger&.error "OIDC auto user creation failed for #{oidc_email_address}: #{e.message}"
nil
end

def derive_user_names_from_oidc(oidc_name, oidc_email_address)
raw_name = oidc_name.to_s.strip
if raw_name.present?
first_name, last_name = raw_name.split(/\s+/, 2)
else
local_part = oidc_email_address.to_s.split("@", 2).first.to_s
fallback_name = local_part.tr("._-", " ").strip
first_name, last_name = fallback_name.split(/\s+/, 2)
end

first_name = first_name.presence || "OIDC"
last_name = last_name.presence || first_name
[first_name, last_name]
end

def auto_create_organization_for(user, config, logger)
organization_name = config.auto_created_organization_name.presence || "My organization"
organization = Organization.new(name: organization_name, owner: user)
organization.save!
organization.organization_users.create!(user: user, admin: true, all_servers: true)
logger&.info "OIDC auto organization creation succeeded for user #{user.id} (organization ID: #{organization.id})"
organization
rescue ActiveRecord::RecordInvalid => e
logger&.error "OIDC auto organization creation failed for user #{user.id}: #{e.message}"
nil
end

end

end
3 changes: 3 additions & 0 deletions doc/config/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ This document contains all the environment variables which are available for thi
| `MIGRATION_WAITER_SLEEP_TIME` | Integer | The number of seconds to wait between each migration check | 2 |
| `OIDC_ENABLED` | Boolean | Enable OIDC authentication | false |
| `OIDC_LOCAL_AUTHENTICATION_ENABLED` | Boolean | When enabled, users with passwords will still be able to login locally. If disable, only OpenID Connect will be available. | true |
| `OIDC_AUTO_CREATE_USERS` | Boolean | Automatically create a user record when a valid OIDC login is received but no matching user exists | false |
| `OIDC_AUTO_CREATE_ORGANIZATION` | Boolean | Automatically create a default organization for auto-created OIDC users | false |
| `OIDC_AUTO_CREATED_ORGANIZATION_NAME` | String | Name to assign to automatically created organizations | My organization |
| `OIDC_NAME` | String | The name of the OIDC provider as shown in the UI | OIDC Provider |
| `OIDC_ISSUER` | String | The OIDC issuer URL | |
| `OIDC_IDENTIFIER` | String | The client ID for OIDC | |
Expand Down
6 changes: 6 additions & 0 deletions doc/config/yaml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ oidc:
enabled: false
# When enabled, users with passwords will still be able to login locally. If disable, only OpenID Connect will be available.
local_authentication_enabled: true
# Automatically create a user record when a valid OIDC login is received but no user exists
auto_create_users: false
# Automatically create a default organization for auto-created users
auto_create_organization: false
# The name to use for automatically created organizations
auto_created_organization_name: My organization
# The name of the OIDC provider as shown in the UI
name: OIDC Provider
# The OIDC issuer URL
Expand Down
28 changes: 0 additions & 28 deletions docker-compose.yml

This file was deleted.

15 changes: 15 additions & 0 deletions lib/postal/config_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,21 @@ module Postal
default true
end

boolean :auto_create_users do
description "Automatically create a user record when a valid OIDC login is received but no matching user exists"
default false
end

boolean :auto_create_organization do
description "Automatically create a default organization for auto-created OIDC users"
default false
end

string :auto_created_organization_name do
description "The name to use for automatically created organizations"
default "My organization"
end

string :name do
description "The name of the OIDC provider as shown in the UI"
default "OIDC Provider"
Expand Down
87 changes: 81 additions & 6 deletions spec/models/user/oidc_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
allow(Postal::Config.oidc).to receive(:enabled?).and_return(true)
allow(Postal::Config.oidc).to receive(:issuer).and_return(issuer)
allow(Postal::Config.oidc).to receive(:email_address_field).and_return("email")
allow(Postal::Config.oidc).to receive(:uid_field).and_return("sub")
allow(Postal::Config.oidc).to receive(:name_field).and_return("name")
allow(Postal::Config.oidc).to receive(:auto_create_users?).and_return(false)
allow(Postal::Config.oidc).to receive(:auto_create_organization?).and_return(false)
allow(Postal::Config.oidc).to receive(:auto_created_organization_name).and_return("My organization")
end

let(:uid) { "abcdef" }
Expand Down Expand Up @@ -53,6 +58,17 @@
expect(@existing_user.email_address).to eq "test@example.com"
end

context "when the OIDC name contains a single word" do
let(:oidc_name) { "johnny" }

it "duplicates the value for the last name" do
result
@existing_user.reload
expect(@existing_user.first_name).to eq "johnny"
expect(@existing_user.last_name).to eq "johnny"
end
end

it "logs" do
result
expect(logger).to have_logged(/found user with UID abcdef/i)
Expand Down Expand Up @@ -100,14 +116,73 @@
end

context "when there is no user which matches the email address" do
it "returns nil" do
expect(result).to be_nil
context "and auto creation is disabled" do
it "returns nil" do
expect(result).to be_nil
end

it "logs" do
result
expect(logger).to have_logged(/no user with UID abcdef/)
expect(logger).to have_logged(/no user with e-mail address/)
end
end

it "logs" do
result
expect(logger).to have_logged(/no user with UID abcdef/)
expect(logger).to have_logged(/no user with e-mail address/)
context "and auto creation is enabled" do
before do
allow(Postal::Config.oidc).to receive(:auto_create_users?).and_return(true)
end

it "creates a new user" do
expect { result }.to change(User, :count).by(1)
expect(result.email_address).to eq oidc_email
expect(result.oidc_uid).to eq uid
expect(result.oidc_issuer).to eq issuer
expect(result.first_name).to eq "John"
expect(result.last_name).to eq "Smith"
end

it "logs the creation" do
result
expect(logger).to have_logged(/OIDC auto user creation succeeded/i)
end

context "when no name is provided" do
let(:oidc_name) { nil }

it "derives a name from the e-mail address" do
expect(result.first_name).to eq "test"
expect(result.last_name).to eq "test"
end
end

context "when no e-mail is provided" do
let(:oidc_email) { nil }

it "cannot create a user" do
expect(result).to be_nil
expect(logger).to have_logged(/no e-mail address provided/)
end
end

context "when organization auto creation is enabled" do
let(:organization_name) { "My organization" }

before do
allow(Postal::Config.oidc).to receive(:auto_create_organization?).and_return(true)
allow(Postal::Config.oidc).to receive(:auto_created_organization_name).and_return(organization_name)
end

it "creates an organization owned by the new user" do
expect { result }.to change(Organization, :count).by(1)
organization = Organization.last
expect(organization.name).to eq organization_name
expect(organization.owner).to eq result
expect(result.organizations).to include(organization)
expect(organization.organization_users.first.user).to eq result
expect(organization.organization_users.first.admin).to be true
end
end
end
end
end
Expand Down
Loading