Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ The libraries included in the Starter Kit are documented in their respective REA

- [Contact/PhoneNumber](src/Contact/README.md): A library for parsing and formatting a phone number.
- [CUAuth](src/CUAuth/README.md): A middleware for authorizing Laravel users, mostly for Apache mod_shib authentication.
- [Ldap/LdapData](src/Ldap/README.md): A service for retrieving Cornell University LDAP data.


## Deploying a site
Once a Media3 site has been created, you have confirmed you can reach the default site via a web browser, and you have access to the site login by command line, the code can be deployed.
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "library",
"require": {
"php": "^8.2",
"ext-ldap": "*",
"livewire/livewire": "^3.5",
"spatie/laravel-package-tools": "^1.18",
"cubear/cwd_framework_lite": "^3.0",
Expand All @@ -30,7 +31,8 @@
"laravel": {
"providers": [
"CornellCustomDev\\LaravelStarterKit\\StarterKitServiceProvider",
"CornellCustomDev\\LaravelStarterKit\\CUAuth\\CUAuthServiceProvider"
"CornellCustomDev\\LaravelStarterKit\\CUAuth\\CUAuthServiceProvider",
"CornellCustomDev\\LaravelStarterKit\\Ldap\\LdapDataServiceProvider"
]
}
},
Expand Down
42 changes: 42 additions & 0 deletions config/ldap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

return [
/*
|----------------------------------------------------------------------
| LDAP Credentials
|----------------------------------------------------------------------
|
| The credentials used to connect to the LDAP server, which must be set
| in the .env file.
|
| Note that LDAP_USER must be a fully-qualified DN, so we append the
| default Cornell base DN for authorized users.
|
*/
'user' => env('LDAP_USER') ? env('LDAP_USER').',ou=Directory Administrators,o=Cornell University,c=US' : '',
'pass' => env('LDAP_PASS') ?? '',

/*
|----------------------------------------------------------------------
| LDAP Server
|----------------------------------------------------------------------
|
| The LDAP server to connect to and base_dn, defaulting to the Cornell
| LDAP server and base DN.
|
*/
'server' => env('LDAP_SERVER', 'ldaps://query.directory.cornell.edu'),
'base_dn' => env('LDAP_BASE_DN', 'ou=People,o=Cornell University,c=US'),

/*
|----------------------------------------------------------------------
| LDAP Cache
|----------------------------------------------------------------------
|
| The number of seconds to cache LDAP queries. This is useful for
| performance, but be careful not to cache too long, as LDAP data
| can change frequently.
|
*/
'cache_seconds' => env('LDAP_CACHE_SECONDS', 300),
];
8 changes: 8 additions & 0 deletions project/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,11 @@ MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

VITE_APP_NAME="${APP_NAME}"

# LDAP_USER will be appended with ',ou=Directory Administrators,o=Cornell University,c=US'
# If you need to override this, publish the ldap config file and adjust ldap.user.
LDAP_USER=
LDAP_PASS=
#LDAP_SERVER="ldaps://query.directory.cornell.edu"
#LDAP_BASE_DN="ou=People,o=Cornell University,c=US"
#LDAP_CACHE_SECONDS=300
2 changes: 2 additions & 0 deletions project/.lando.local.dev.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## This file is for local development testing of the LaravelStarterKit in a project directory
## It should be added to a `.lando.local.dev.yml
services:
appserver:
overrides:
Expand Down
146 changes: 146 additions & 0 deletions src/Ldap/LdapData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

namespace CornellCustomDev\LaravelStarterKit\Ldap;

/**
* An immutable data object representing the LDAP data returned for a user.
*/
class LdapData
{
public function __construct(
public string $uid,
public string $principalName,
public string $emplid,
public ?string $firstName,
public ?string $lastName,
public ?string $displayName,
public ?string $email,
public ?string $campusPhone,
public ?string $deptName,
public ?string $workingTitle,
public ?string $primaryAffiliation,
public ?array $affiliations,
public ?array $previousNetids,
public ?array $previousEmplids,
public array $ldapData = [],
public array $returnedData = []
) {}

/**
* Create a new LdapData object from an array of LDAP data.
*
* See https://confluence.cornell.edu/display/IDM/Attributes
*
* @param array $data The LDAP data array, typically from a search result.
* @param bool $withLdapData Whether to include the LDAP data in the returned object.
*/
public static function make(array $data, bool $withLdapData = true): ?LdapData
{
// Use preferred first name if it is not null, otherwise use givenName.
$firstName = ($data['cornelleduprefgivenname'] ?? null) ?: ($data['givenName'] ?? null);

// Use preferred last name if it is not null, otherwise use sn.
$lastName = ($data['cornelleduprefsn'] ?? null) ?: ($data['sn'] ?? null);

// Affiliations can be a string, an array, or not set
$affiliationCollection = collect(($data['cornelleduaffiliation'] ?? null) ?: null);
$affiliations = $affiliationCollection->toArray();

// Use the defined primary affiliation or the first affiliation in the collection.
$primaryAffiliation = ($data['cornelleduprimaryaffiliation'] ?? null) ?: $affiliationCollection->shift() ?? '';

// Secondary affiliation is the first affiliation that is not the primary affiliation.
$secondaryAffiliation = $affiliationCollection->reject($primaryAffiliation)->first() ?? '';

// Process previousNetids: always convert the comma-separated string to an array.
$previousNetIds = collect(explode(',', $data['cornelledupreviousnetids'] ?? ''))
->map(fn ($id) => trim($id))->filter()->all();

// Same for previousEmplids
$previousEmplids = collect(explode(',', $data['cornelledupreviousemplids'] ?? ''))
->map(fn ($id) => trim($id))->filter()->all();

// User may have exercised FERPA right to suppress name.
if (empty($firstName) && empty($lastName)) {
$firstName = 'Cornell';
$lastName = $primaryAffiliation === 'student' ? 'Student' : 'User';
}

// Format an array to match the old LDAP::data function
$ldapData = [
'emplid' => $data['cornelleduemplid'] ?? '',
'firstname' => $firstName ?? '',
'lastname' => $lastName ?? '',
'name' => trim("$firstName $lastName"),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think full name must include middle name or at least it should be an option for both options. Also name formatting is usually application task: some applications need to display last name fist, some - first name first

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The array that is being populated is only there to match the LDAP::data() function that this is intended to replace.

This $ldapData = [... chunk could be removed entirely if we don't want to use the library for supporting existing applications.

'email' => $data['mail'] ?? '',
'campusphone' => $data['cornelleducampusphone'] ?? '',
'netid' => $data['uid'],
'deptname' => $data['cornelledudeptname1'] ?? '',
'primaryaffiliation' => $primaryAffiliation,
'secondaryaffiliation' => $secondaryAffiliation,
'wrkngtitle' => $data['cornelleduwrkngtitle1'] ?? '',
'affiliations' => $affiliations,
'previousnetids' => $data['cornelledupreviousnetids'] ?? null,
'previousemplids' => $data['cornelledupreviousemplids'] ?? null,
];
Comment on lines +84 to +85

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to return all these data from LDAP? Most of the time we just need to get a name and netid. I think the BindID must have special permissions to return some of these data (like emplid) and usually we do not have and do not need those permissions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to return all the data from LDAP: No and most queries won't, which is fine and the code requires only 'uid' to be populated in the results. (And even that can be made optional.)

The attribute list in here was meant as a starter list, merging what is in DailyCheck, IT Gov, and EMN phone, which happened to be three place I was working at the time I was putting this together. Looking at the LDAP names in Confluence, I see that indeed emplid is not public, so you are right about special permission on that.


return new LdapData(
uid: $data['uid'],
principalName: $data['edupersonprincipalname']
?? $data['uid'].'@cornell.edu',
emplid: $data['cornelleduemplid'] ?? '',

firstName: $firstName,
lastName: $lastName,

// Use preferred display name if it is not null, otherwise fall back on first_name + last_name.
displayName: $data['displayname']
?? trim($firstName.' '.$lastName),

// Only set 'email' if it is not empty.
email: ($data['mail'] ?? null) ?: null,

campusPhone: $data['cornelleducampusphone'] ?? null,
deptName: $data['cornelledudeptname1'] ?? null,
workingTitle: $data['cornelleduwrkngtitle1'] ?? null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if an application needs department name and working title, then these data should be coming from Workday not from LDAP

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are bringing up a good question about the source of the data. I can see that the attribute documentation indicates the those fields come from "HR" (i.e., Workday), but I don't know much else.

Do we have reason to be concerned about some of these pass-through fields? Looking at the sources for the attributes, lots of them come from Workday and Peoplesoft.

primaryAffiliation: $primaryAffiliation ?: null,
affiliations: $affiliations,
previousNetids: $previousNetIds ?: null,
previousEmplids: $previousEmplids ?: null,
ldapData: $withLdapData ? $ldapData : [],
returnedData: $withLdapData ? $data : [],
);
}

/**
* Provides an id that is unique within the IdP, i.e., NetID or CWID
*/
public function id(): string
{
return $this->uid;
}

/*
* Returns the eduPersonPrincipalName
*/
public function principalName(): string
{
return $this->principalName;
}

/**
* Returns the primary email (netid@cornell.edu) if available, otherwise the alias email.
*/
public function email(): string
{
return $this->email ?: $this->principalName;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can alias email exists without primary email?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! The doc comment is backwards 🤦🏻‍♂️

I think this confusion (mine, yours) highlights that we should be very clear about the function names. Maybe:

  • emailAlias() : hopefully obvious, and should be null if there isn't one
  • mail(): which is simply the direct value from LDAP (alias if it exists, or netid@cornell.edu, if user has email)
  • principalEmail(): just making this name up... but meant to be the edupersonprincipalname attribute which looks like netid@cornell.edu for Ithaca people


/**
* Returns the display name if available, otherwise the common name, fallback is "givenName sn".
*/
public function name(): string
{
return $this->displayName;
}
}
13 changes: 13 additions & 0 deletions src/Ldap/LdapDataException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace CornellCustomDev\LaravelStarterKit\Ldap;

use ErrorException;

/**
* An exception thrown by LdapService.
*/
class LdapDataException extends ErrorException
{
// This class is intentionally empty.
}
28 changes: 28 additions & 0 deletions src/Ldap/LdapDataServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace CornellCustomDev\LaravelStarterKit\Ldap;

use CornellCustomDev\LaravelStarterKit\StarterKitServiceProvider;
use Illuminate\Support\ServiceProvider;

class LdapDataServiceProvider extends ServiceProvider
{
const INSTALL_CONFIG_TAG = 'starterkit-ldap-config';

public function register(): void
{
$this->mergeConfigFrom(
path: __DIR__.'/../../config/ldap.php',
key: 'ldap',
);
}

public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/../../config/ldap.php' => config_path('ldap.php'),
], StarterKitServiceProvider::PACKAGE_NAME.':'.self::INSTALL_CONFIG_TAG);
}
}
}
Loading